diff --git a/opentrafficshaper/plugins/configmanager.pm b/opentrafficshaper/plugins/configmanager.pm index b7889b6f9c9d9fa06bb547ae58c659cb6269e525..281a128e84171cfc80c6914994aa53dbd9c40836 100644 --- a/opentrafficshaper/plugins/configmanager.pm +++ b/opentrafficshaper/plugins/configmanager.pm @@ -37,12 +37,22 @@ our (@ISA,@EXPORT,@EXPORT_OK); @EXPORT = qw( ); @EXPORT_OK = qw( + getLimits + setLimitAttribute + getLimitAttribute + + getShaperState + setShaperState + + getTrafficClasses + + getPriorityName ); use constant { VERSION => '0.0.1', - # After how long does a user get removed if he's offline + # After how long does a limit get removed if its's deemed offline TIMEOUT_EXPIRE_OFFLINE => 300, # How often our config check ticks @@ -85,10 +95,11 @@ my $default_pool_priority = 10; # Pending changes my $changeQueue = { }; -# UserID counter -my $userIPMap = {}; -my $userIDMap = {}; -my $userIDCounter = 1; +# Limits +my $limits = { }; +my $limitIPMap = { }; +my $limitIDMap = { }; +my $limitIDCounter = 1; @@ -218,9 +229,6 @@ sub session_tick { my $kernel = $_[KERNEL]; - # Suck in global - my $users = $globals->{'users'}; - # Now my $now = time(); @@ -229,186 +237,186 @@ sub session_tick { # LOOP WITH CHANGES # - foreach my $uid (keys %{$changeQueue}) { - # Changes for user + foreach my $lid (keys %{$changeQueue}) { + # Changes for limit # Minimum required info is: # - Username # - IP # - Status # - LastUpdate - my $cuser = $changeQueue->{$uid}; + my $climit = $changeQueue->{$lid}; # - # USER IN LIST + # LIMIT IN LIST # - if (defined(my $guser = $users->{$uid})) { + if (defined(my $glimit = $limits->{$lid})) { - # This is a new user notification - if ($cuser->{'Status'} eq "new") { - $logger->log(LOG_INFO,"[CONFIGMANAGER] User '$cuser->{'Username'}' [$uid], user already live but new state provided?"); + # This is a new limit notification + if ($climit->{'Status'} eq "new") { + $logger->log(LOG_INFO,"[CONFIGMANAGER] Limit '$climit->{'Username'}' [$lid], limit already live but new state provided?"); # Get the changes we made and push them to the shaper - if (my $changes = processChanges($guser,$cuser)) { + if (my $changes = processChanges($glimit,$climit)) { # Post to shaper - $kernel->post("shaper" => "change" => $uid => $changes); + $kernel->post("shaper" => "change" => $lid => $changes); } # Remove from change queue - delete($changeQueue->{$uid}); + delete($changeQueue->{$lid}); # Online or "ping" status notification - } elsif ($cuser->{'Status'} eq "online") { - $logger->log(LOG_DEBUG,"[CONFIGMANAGER] User '$cuser->{'Username'}' [$uid], user still online"); + } elsif ($climit->{'Status'} eq "online") { + $logger->log(LOG_DEBUG,"[CONFIGMANAGER] Limit '$climit->{'Username'}' [$lid], limit still online"); # Get the changes we made and push them to the shaper - if (my $changes = processChanges($guser,$cuser)) { + if (my $changes = processChanges($glimit,$climit)) { # Post to shaper - $kernel->post("shaper" => "change" => $uid => $changes); + $kernel->post("shaper" => "change" => $lid => $changes); } # Remove from change queue - delete($changeQueue->{$uid}); + delete($changeQueue->{$lid}); # Offline notification, this we going to treat specially - } elsif ($cuser->{'Status'} eq "offline") { + } elsif ($climit->{'Status'} eq "offline") { # We first check if this update was received some time ago, and if it exceeds our expire time - # We don't want to immediately remove a user, only for him to come back on a few seconds later, the cost in exec()'s + # We don't want to immediately remove a limit, only for him to come back on a few seconds later, the cost in exec()'s # would be pretty high - if ($now - $cuser->{'LastUpdate'} > TIMEOUT_EXPIRE_OFFLINE) { + if ($now - $climit->{'LastUpdate'} > TIMEOUT_EXPIRE_OFFLINE) { # Remove entry if no longer live - if ($guser->{'_shaper.state'} == SHAPER_NOTLIVE) { - $logger->log(LOG_INFO,"[CONFIGMANAGER] User '$cuser->{'Username'}' [$uid] offline and removed from shaper"); + if ($glimit->{'_shaper.state'} == SHAPER_NOTLIVE) { + $logger->log(LOG_INFO,"[CONFIGMANAGER] Limit '$climit->{'Username'}' [$lid] offline and removed from shaper"); # Remove from system - delete($users->{$uid}); + delete($limits->{$lid}); # Remove from change queue - delete($changeQueue->{$uid}); + delete($changeQueue->{$lid}); # Set this UID as no longer using this IP - # NK: If we try remove it before the user is actually removed we could get a reconnection causing this value - # to be totally gone, which means we not tracking this user using this IP anymore, not easily solved!! - delete($userIPMap->{$guser->{'IP'}}->{$uid}); + # NK: If we try remove it before the limit is actually removed we could get a reconnection causing this value + # to be totally gone, which means we not tracking this limit using this IP anymore, not easily solved!! + delete($limitIPMap->{$glimit->{'IP'}}->{$lid}); # Check if we can delete the IP too - if (keys %{$userIPMap->{$guser->{'IP'}}} == 0) { - delete($userIPMap->{$guser->{'IP'}}); + if (keys %{$limitIPMap->{$glimit->{'IP'}}} == 0) { + delete($limitIPMap->{$glimit->{'IP'}}); } # Next record, we don't want to do any updates below next; # Push to shaper - } elsif ($guser->{'_shaper.state'} == SHAPER_LIVE) { - $logger->log(LOG_DEBUG,"[CONFIGMANAGER] User '$cuser->{'Username'}' [$uid] offline, queue remove from shaper"); + } elsif ($glimit->{'_shaper.state'} == SHAPER_LIVE) { + $logger->log(LOG_DEBUG,"[CONFIGMANAGER] Limit '$climit->{'Username'}' [$lid] offline, queue remove from shaper"); # Post removal to shaper - $kernel->post("shaper" => "remove" => $uid); + $kernel->post("shaper" => "remove" => $lid); } else { - $logger->log(LOG_DEBUG,"[CONFIGMANAGER] User '$cuser->{'Username'}' [$uid], user in list, but offline now and". + $logger->log(LOG_DEBUG,"[CONFIGMANAGER] Limit '$climit->{'Username'}' [$lid], limit in list, but offline now and". " expired, still live, waiting for shaper"); } } } - # Update the user data - $guser->{'Status'} = $cuser->{'Status'}; - $guser->{'LastUpdate'} = $cuser->{'LastUpdate'}; + # Update the limit data + $glimit->{'Status'} = $climit->{'Status'}; + $glimit->{'LastUpdate'} = $climit->{'LastUpdate'}; # This item is optional - $guser->{'Expires'} = $cuser->{'Expires'} if (defined($cuser->{'Expires'})); + $glimit->{'Expires'} = $climit->{'Expires'} if (defined($climit->{'Expires'})); # - # USER NOT IN LIST + # LIMIT NOT IN LIST # } else { - # We take new and online notifications the same way here if the user is not in our global userlist already - if (($cuser->{'Status'} eq "new" || $cuser->{'Status'} eq "online")) { - $logger->log(LOG_DEBUG,"[CONFIGMANAGER] Processing new user '$cuser->{'Username'}' [$uid]"); + # We take new and online notifications the same way here if the limit is not in our global limit list already + if (($climit->{'Status'} eq "new" || $climit->{'Status'} eq "online")) { + $logger->log(LOG_DEBUG,"[CONFIGMANAGER] Processing new limit '$climit->{'Username'}' [$lid]"); # We first going to look for IP conflicts... - my @ipUsers = keys %{$userIPMap->{$cuser->{'IP'}}}; + my @ipLimits = keys %{$limitIPMap->{$climit->{'IP'}}}; if ( # If there is already an entry and its not us ... - ( @ipUsers == 1 && !defined($userIPMap->{$cuser->{'IP'}}->{$uid}) ) + ( @ipLimits == 1 && !defined($limitIPMap->{$climit->{'IP'}}->{$lid}) ) # Or if there is more than 1 entry... - || @ipUsers > 1 + || @ipLimits > 1 ) { # We not going to post this to the shaper, but we are going to override the status - $cuser->{'Status'} = 'conflict'; - $cuser->{'_shaper.state'} = SHAPER_NOTLIVE; + $climit->{'Status'} = 'conflict'; + $climit->{'_shaper.state'} = SHAPER_NOTLIVE; # Give a bit of info my @conflictUsernames; - foreach my $uid (@ipUsers) { - push(@conflictUsernames,$users->{$uid}->{'Username'}); + foreach my $lid (@ipLimits) { + push(@conflictUsernames,$limits->{$lid}->{'Username'}); } # Output log line - $logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process user '".$cuser->{'Username'}."' IP '$cuser->{'IP'}' conflicts with users '". + $logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process limit '".$climit->{'Username'}."' IP '$climit->{'IP'}' conflicts with users '". join(',',@conflictUsernames)."'."); - # We cannot trust shaping when there is more than 1 user on the IP, so we going to remove all users with this + # We cannot trust shaping when there is more than 1 limit on the IP, so we going to remove all limits with this # IP from the shaper below... - foreach my $uid2 (@ipUsers) { - # Check if the user has been setup already (all but the user we busy with, as its setup below) - if (defined($userIPMap->{$cuser->{'IP'}}->{$uid2})) { - my $guser2 = $users->{$uid2}; - - # If the user is active or pending on the shaper, remove it - if ($guser2->{'_shaper.state'} == SHAPER_LIVE || $guser2->{'_shaper.state'} == SHAPER_PENDING) { - $logger->log(LOG_WARN,"[CONFIGMANAGER] Removing conflicted user '".$guser2->{'Username'}."' [$uid2] from shaper'"); + foreach my $lid2 (@ipLimits) { + # Check if the limit has been setup already (all but the limit we busy with, as its setup below) + if (defined($limitIPMap->{$climit->{'IP'}}->{$lid2})) { + my $glimit2 = $limits->{$lid2}; + + # If the limit is active or pending on the shaper, remove it + if ($glimit2->{'_shaper.state'} == SHAPER_LIVE || $glimit2->{'_shaper.state'} == SHAPER_PENDING) { + $logger->log(LOG_WARN,"[CONFIGMANAGER] Removing conflicted limit '".$glimit2->{'Username'}."' [$lid2] from shaper'"); # Post removal to shaper - $kernel->post("shaper" => "remove" => $uid2); - # Update that we're offline directly to global user table - $guser2->{'Status'} = 'conflict'; + $kernel->post("shaper" => "remove" => $lid2); + # Update that we're offline directly to global limit table + $glimit2->{'Status'} = 'conflict'; } } } - # All looks good, no conflicts, we're set to add this user! + # All looks good, no conflicts, we're set to add this limit! } else { - # Post to the user to the shaper - $cuser->{'_shaper.state'} = SHAPER_PENDING; - $kernel->post("shaper" => "add" => $uid); + # Post to the limit to the shaper + $climit->{'_shaper.state'} = SHAPER_PENDING; + $kernel->post("shaper" => "add" => $lid); } # Set this UID as using this IP - $userIPMap->{$cuser->{'IP'}}->{$uid} = 1; + $limitIPMap->{$climit->{'IP'}}->{$lid} = 1; # This is now live - $users->{$uid} = $cuser; + $limits->{$lid} = $climit; - # User is not in our list and this is an unknown state we're trasitioning to + # Limit is not in our list and this is an unknown state we're trasitioning to } else { - $logger->log(LOG_DEBUG,"[CONFIGMANAGER] Ignoring user '$cuser->{'Username'}' [$uid] state '$cuser->{'Status'}', not in our". + $logger->log(LOG_DEBUG,"[CONFIGMANAGER] Ignoring limit '$climit->{'Username'}' [$lid] state '$climit->{'Status'}', not in our". " global list"); } # Remove from change queue - delete($changeQueue->{$uid}); + delete($changeQueue->{$lid}); } } # - # CHECK OUT CONNECTED USERS + # CHECK OUT CONNECTED LIMITS # - foreach my $uid (keys %{$users}) { - # Global user - my $guser = $users->{$uid}; - - # Check for expired users - if ($guser->{'Expires'} != 0 && $guser->{'Expires'} < $now) { - $logger->log(LOG_INFO,"[CONFIGMANAGER] User '$guser->{'Username'}' has expired, marking offline"); - # Looks like this user has expired? - my $cuser = { - 'Username' => $guser->{'Username'}, - 'IP' => $guser->{'IP'}, + foreach my $lid (keys %{$limits}) { + # Global limit + my $glimit = $limits->{$lid}; + + # Check for expired limits + if ($glimit->{'Expires'} != 0 && $glimit->{'Expires'} < $now) { + $logger->log(LOG_INFO,"[CONFIGMANAGER] Limit '$glimit->{'Username'}' has expired, marking offline"); + # Looks like this limit has expired? + my $climit = { + 'Username' => $glimit->{'Username'}, + 'IP' => $glimit->{'IP'}, 'Status' => 'offline', - 'LastUpdate' => $guser->{'LastUpdate'}, + 'LastUpdate' => $glimit->{'LastUpdate'}, }; # Add to change queue - $changeQueue->{$uid} = $cuser; + $changeQueue->{$lid} = $climit; } } @@ -444,73 +452,73 @@ sub session_tick { # - online # - unknown # Source -# - This is the source of the user, typically plugin.ModuleName +# - This is the source of the limit, typically plugin.ModuleName sub process_change { - my ($kernel, $user) = @_[KERNEL, ARG0]; + my ($kernel, $limit) = @_[KERNEL, ARG0]; - # Create a unique user identifier - my $userUniq = $user->{'Username'} . "/" . $user->{'IP'}; + # Create a unique limit identifier + my $limitUniq = $limit->{'Username'} . "/" . $limit->{'IP'}; # If we've not seen it - my $uid; - if (!defined($uid = $userIDMap->{$userUniq})) { - # Give it the next userID in the list - $userIDMap->{$userUniq} = $uid = ++$userIDCounter; + my $lid; + if (!defined($lid = $limitIDMap->{$limitUniq})) { + # Give it the next limitID in the list + $limitIDMap->{$limitUniq} = $lid = ++$limitIDCounter; } # We start off blank so we only pull in whats supported - my $userChange; - if (!($userChange->{'Username'} = $user->{'Username'})) { - $logger->log(LOG_DEBUG,"[CONFIGMANAGER] Cannot process user change as username is invalid."); + my $limitChange; + if (!($limitChange->{'Username'} = $limit->{'Username'})) { + $logger->log(LOG_DEBUG,"[CONFIGMANAGER] Cannot process limit change as username is invalid."); } - $userChange->{'Username'} = $user->{'Username'}; - $userChange->{'IP'} = $user->{'IP'}; + $limitChange->{'Username'} = $limit->{'Username'}; + $limitChange->{'IP'} = $limit->{'IP'}; # Check group is OK - if (!($userChange->{'GroupID'} = checkGroupID($user->{'GroupID'}))) { - $logger->log(LOG_DEBUG,"[CONFIGMANAGER] Cannot process user change for '".$user->{'Username'}."' as the GroupID is invalid."); + if (!($limitChange->{'GroupID'} = checkGroupID($limit->{'GroupID'}))) { + $logger->log(LOG_DEBUG,"[CONFIGMANAGER] Cannot process limit change for '".$limit->{'Username'}."' as the GroupID is invalid."); } # Check class is OK - if (!($userChange->{'ClassID'} = checkClassID($user->{'ClassID'}))) { - $logger->log(LOG_DEBUG,"[CONFIGMANAGER] Cannot process user change for '".$user->{'Username'}."' as the ClassID is invalid."); + if (!($limitChange->{'ClassID'} = checkClassID($limit->{'ClassID'}))) { + $logger->log(LOG_DEBUG,"[CONFIGMANAGER] Cannot process limit change for '".$limit->{'Username'}."' as the ClassID is invalid."); } - $userChange->{'TrafficLimitTx'} = $user->{'TrafficLimitTx'}; - $userChange->{'TrafficLimitRx'} = $user->{'TrafficLimitRx'}; + $limitChange->{'TrafficLimitTx'} = $limit->{'TrafficLimitTx'}; + $limitChange->{'TrafficLimitRx'} = $limit->{'TrafficLimitRx'}; # Take base limits if we don't have any burst values set - $userChange->{'TrafficLimitTxBurst'} = $user->{'TrafficLimitTxBurst'}; - $userChange->{'TrafficLimitRxBurst'} = $user->{'TrafficLimitRxBurst'}; + $limitChange->{'TrafficLimitTxBurst'} = $limit->{'TrafficLimitTxBurst'}; + $limitChange->{'TrafficLimitRxBurst'} = $limit->{'TrafficLimitRxBurst'}; # If we don't have burst limits, set them to the traffic limit, and reset the limit to 25% - if (!defined($userChange->{'TrafficLimitTxBurst'})) { - $userChange->{'TrafficLimitTxBurst'} = $userChange->{'TrafficLimitTx'}; - $userChange->{'TrafficLimitTx'} = int($userChange->{'TrafficLimitTxBurst'}/4); + if (!defined($limitChange->{'TrafficLimitTxBurst'})) { + $limitChange->{'TrafficLimitTxBurst'} = $limitChange->{'TrafficLimitTx'}; + $limitChange->{'TrafficLimitTx'} = int($limitChange->{'TrafficLimitTxBurst'}/4); } - if (!defined($userChange->{'TrafficLimitRxBurst'})) { - $userChange->{'TrafficLimitRxBurst'} = $userChange->{'TrafficLimitRx'}; - $userChange->{'TrafficLimitRx'} = int($userChange->{'TrafficLimitRxBurst'}/4); + if (!defined($limitChange->{'TrafficLimitRxBurst'})) { + $limitChange->{'TrafficLimitRxBurst'} = $limitChange->{'TrafficLimitRx'}; + $limitChange->{'TrafficLimitRx'} = int($limitChange->{'TrafficLimitRxBurst'}/4); } # Optional priority, we default to 5 - $userChange->{'TrafficPriority'} = defined($user->{'TrafficPriority'}) ? $user->{'TrafficPriority'} : 5; + $limitChange->{'TrafficPriority'} = defined($limit->{'TrafficPriority'}) ? $limit->{'TrafficPriority'} : 5; # Set when this entry expires - $userChange->{'Expires'} = defined($user->{'Expires'}) ? $user->{'Expires'} : 0; + $limitChange->{'Expires'} = defined($limit->{'Expires'}) ? $limit->{'Expires'} : 0; # Check status is OK - if (!($userChange->{'Status'} = checkStatus($user->{'Status'}))) { - $logger->log(LOG_DEBUG,"[CONFIGMANAGER] Cannot process user change for '".$user->{'Username'}."' as the Status is invalid."); + if (!($limitChange->{'Status'} = checkStatus($limit->{'Status'}))) { + $logger->log(LOG_DEBUG,"[CONFIGMANAGER] Cannot process user change for '".$limit->{'Username'}."' as the Status is invalid."); } - $userChange->{'Source'} = $user->{'Source'}; + $limitChange->{'Source'} = $limit->{'Source'}; # Set the user ID before we post to the change queue - $userChange->{'ID'} = $uid; - $userChange->{'LastUpdate'} = time(); + $limitChange->{'ID'} = $lid; + $limitChange->{'LastUpdate'} = time(); # Push change to change queue - $changeQueue->{$uid} = $userChange; + $changeQueue->{$lid} = $limitChange; } @@ -558,6 +566,63 @@ sub checkStatus return undef; } +# Function to return list of limits +sub getLimits +{ + return $limits; +} + +# Function to set a limit attribute +sub setLimitAttribute +{ + my ($lid,$attr,$value) = @_; + + $limits->{$lid}->{'attributes'}->{$attr} = $value; +} + +# Function to get a limit attribute +sub getLimitAttribute +{ + my ($lid,$attr) = @_; + + + # Check if attribute exists first + if (defined($limits->{$lid}) && defined($limits->{$lid}->{'attributes'}) && defined($limits->{$lid}->{'attributes'}->{$attr})) { + return $limits->{$lid}->{'attributes'}->{$attr}; + } else { + return undef; + } +} + +# Function to set shaper state on a limit +sub setShaperState +{ + my ($lid,$state) = @_; + + $limits->{$lid}->{'_shaper.state'} = $state; +} + +# Function to get shaper state on a limit +sub getShaperState +{ + my $lid = shift; + + return $limits->{$lid}->{'_shaper.state'}; +} + +# Function to get traffic classes +sub getTrafficClasses +{ + return $classes; +} + +# Function to get priority name +sub getPriorityName +{ + my $prio = shift; + + return $classes->{$prio}; +} # Handle SIGHUP diff --git a/opentrafficshaper/plugins/statistics/statistics.pm b/opentrafficshaper/plugins/statistics/statistics.pm index 731242a74999cd5330f2e2921c15938365e2bbc5..7ee3b88204758a90938568338702560c0963c48c 100644 --- a/opentrafficshaper/plugins/statistics/statistics.pm +++ b/opentrafficshaper/plugins/statistics/statistics.pm @@ -28,6 +28,7 @@ use opentrafficshaper::constants; use opentrafficshaper::logger; use opentrafficshaper::utils; +use opentrafficshaper::plugins::configmanager qw( getLimits ); # Exporter stuff @@ -37,6 +38,7 @@ our (@ISA,@EXPORT,@EXPORT_OK); @EXPORT = qw( ); @EXPORT_OK = qw( + getLastStats ); use constant { @@ -138,7 +140,7 @@ sub session_init { $logger->log(LOG_DEBUG,"[STATISTICS] Initialized"); } -# Update users Statistics +# Update limit Statistics # $uid has some special use cases: # main:$iface:all - Interface total stats # main:$iface:classes - Interface classified traffic @@ -147,18 +149,21 @@ sub do_update { my ($kernel, $item, $stats) = @_[KERNEL, ARG0, ARG1]; - # Save entry - $statsCache->{$item}->{$stats->{'timestamp'}} = $stats; - # Buffer size $logger->log(LOG_INFO,"[STATISTICS] Statistics update for '%s', buffered '%s' items",$item,scalar keys %{$statsCache->{$item}}); if ($item =~ /^main/) { } else { # Pull in global - my $users = $globals->{'users'}; - my $user = $users->{$item}; - + my $limits = getLimits(); + my $limit = $limits->{$item}; + my $username = $limit->{'Username'}; + + # Save entry + $statsCache->{$username}->{$stats->{'timestamp'}}->{$stats->{'direction'}} = $stats; + +use Data::Dumper; print STDERR "Limit: ".Dumper($limit); +use Data::Dumper; print STDERR "Stats: ".Dumper($stats); } @@ -191,7 +196,8 @@ print STDERR "Pass4: $event\n"; # Handle subscriptions to updates -sub do_subscribe { +sub do_subscribe +{ my ($kernel, $handler, $handlerEvent, $item) = @_[KERNEL, ARG0, ARG1, ARG2]; @@ -202,7 +208,8 @@ sub do_subscribe { # Handle unsubscribes -sub do_unsubscribe { +sub do_unsubscribe +{ my ($kernel, $handler, $handlerEvent, $item) = @_[KERNEL, ARG0, ARG1, ARG2]; @@ -211,11 +218,42 @@ sub do_unsubscribe { delete($subscribers->{$item}->{$handler}->{$handlerEvent}); } +# Return user last stats +sub getLastStats +{ + my $username = shift; + + my $statistics; + + # Do we have stats for this user in the cache? + if (defined($statsCache->{$username})) { + # Grab last entry + my $lastTimestamp = (sort keys %{$statsCache->{$username}})[-1]; + # We should ALWAYS have one, unless the server just booted + if (defined($lastTimestamp)) { + # Loop with both directions + foreach my $direction ('tx','rx') { + # Get a easier to use handle on the stats + if (my $stats = $statsCache->{$username}->{$lastTimestamp}->{$direction}) { + # Setup the statistics hash + $statistics->{$direction} = { + 'current_rate' => $stats->{'current_rate'}, + 'current_pps' => $stats->{'current_pps'}, + }; + } + } + } + } + + return $statistics; +} + sub handle_SIGHUP { $logger->log(LOG_WARN,"[STATISTICS] Got SIGHUP, ignoring for now"); } + 1; # vim: ts=4 diff --git a/opentrafficshaper/plugins/tc/tc.pm b/opentrafficshaper/plugins/tc/tc.pm index a4b56fc049112dcac1127615b7bc07d0c327df84..16937bf85d4d3538181fc3c54ea0f3d16bf523ef 100644 --- a/opentrafficshaper/plugins/tc/tc.pm +++ b/opentrafficshaper/plugins/tc/tc.pm @@ -28,6 +28,10 @@ use opentrafficshaper::constants; use opentrafficshaper::logger; use opentrafficshaper::utils; +use opentrafficshaper::plugins::configmanager qw( + getLimits getLimitAttribute setLimitAttribute + getShaperState setShaperState +); # Exporter stuff @@ -198,17 +202,17 @@ sub session_init { # Add event for tc sub do_add { - my ($kernel,$heap,$uid) = @_[KERNEL, HEAP, ARG0]; + my ($kernel,$heap,$lid) = @_[KERNEL, HEAP, ARG0]; # Pull in global - my $users = $globals->{'users'}; - my $user = $users->{$uid}; + my $limits = getLimits(); + my $limit = $limits->{$lid}; - $logger->log(LOG_DEBUG," Add '$user->{'Username'}' [$uid]\n"); + $logger->log(LOG_DEBUG,"[TC] Add '$limit->{'Username'}' [$lid]\n"); - my @components = split(/\./,$user->{'IP'}); + my @components = split(/\./,$limit->{'IP'}); # Filter level 2-4 my $ip1 = $components[0]; @@ -219,7 +223,7 @@ sub do_add { # Check if we have a entry for the /8, if not we must create our 2nd level hash table and link it if (!defined($tcFilterMappings->{$ip1})) { # Setup IP1's hash table - my $filterID = getTcFilter($uid); + my $filterID = getTcFilter($lid); $tcFilterMappings->{$ip1}->{'id'} = $filterID; @@ -283,7 +287,7 @@ sub do_add { # Check if we have our /16 hash entry, if not we must create the 3rd level hash table if (!defined($tcFilterMappings->{$ip1}->{$ip2})) { - my $filterID = getTcFilter($uid); + my $filterID = getTcFilter($lid); # Set 2nd level hash table ID $tcFilterMappings->{$ip1}->{$ip2}->{'id'} = $filterID; # Grab some hash table ID's we need @@ -350,7 +354,7 @@ sub do_add { # Check if we have our /24 hash entry, if not we must create the 4th level hash table if (!defined($tcFilterMappings->{$ip1}->{$ip2}->{$ip3})) { - my $filterID = getTcFilter($uid); + my $filterID = getTcFilter($lid); # Set 3rd level hash table ID $tcFilterMappings->{$ip1}->{$ip2}->{$ip3}->{'id'} = $filterID; # Grab some hash table ID's we need @@ -419,18 +423,18 @@ sub do_add { # Only if we have limits setup process them - if (defined($user->{'TrafficLimitTx'}) && defined($user->{'TrafficLimitRx'})) { - # Build users tc class ID - my $classID = getTcClass($uid); + if (defined($limit->{'TrafficLimitTx'}) && defined($limit->{'TrafficLimitRx'})) { + # Build limit tc class ID + my $classID = getTcClass($lid); # Grab some hash table ID's we need my $ip3HtHex = $tcFilterMappings->{$ip1}->{$ip2}->{$ip3}->{'id'}; my $ip4Hex = toHex($ip4); # Generate our filter handle my $filterHandle = "${ip3HtHex}:${ip4Hex}:1"; - # Save user tc class ID - $user->{'tc.class'} = $classID; - $user->{'tc.filter'} = "${ip3HtHex}:${ip4Hex}:1"; + # Save limit tc class ID + setLimitAttribute($lid,'tc.class',$classID); + setLimitAttribute($lid,'tc.filter',"${ip3HtHex}:${ip4Hex}:1"); # # SETUP MAIN TRAFFIC LIMITS @@ -443,9 +447,9 @@ sub do_add { 'parent','1:2', 'classid',"1:$classID", 'htb', - 'rate', $user->{'TrafficLimitTx'} . "kbit", - 'ceil', $user->{'TrafficLimitTxBurst'} . "kbit", - 'prio',$user->{'TrafficPriority'}, + 'rate', $limit->{'TrafficLimitTx'} . "kbit", + 'ceil', $limit->{'TrafficLimitTxBurst'} . "kbit", + 'prio', $limit->{'TrafficPriority'}, ]); $kernel->post("_tc" => "queue" => [ '/sbin/tc','class','add', @@ -453,9 +457,9 @@ sub do_add { 'parent','1:2', 'classid',"1:$classID", 'htb', - 'rate', $user->{'TrafficLimitRx'} . "kbit", - 'ceil', $user->{'TrafficLimitRxBurst'} . "kbit", - 'prio',$user->{'TrafficPriority'}, + 'rate', $limit->{'TrafficLimitRx'} . "kbit", + 'ceil', $limit->{'TrafficLimitRxBurst'} . "kbit", + 'prio', $limit->{'TrafficPriority'}, ]); # @@ -472,7 +476,7 @@ sub do_add { 'protocol',$config->{'ip_protocol'}, 'u32', 'ht',"${ip3HtHex}:${ip4Hex}:", - 'match','ip','dst',$user->{'IP'}, + 'match','ip','dst',$limit->{'IP'}, 'at',16+$config->{'iphdr_offset'}, 'flowid',"1:$classID", ]); @@ -485,35 +489,35 @@ sub do_add { 'protocol',$config->{'ip_protocol'}, 'u32', 'ht',"${ip3HtHex}:${ip4Hex}:", - 'match','ip','src',$user->{'IP'}, + 'match','ip','src',$limit->{'IP'}, 'at',12+$config->{'iphdr_offset'}, 'flowid',"1:$classID", ]); - tc_addtask_optimize($kernel,$config->{'txiface'},$classID,$user->{'TrafficLimitTx'}); - tc_addtask_optimize($kernel,$config->{'rxiface'},$classID,$user->{'TrafficLimitRx'}); + tc_addtask_optimize($kernel,$config->{'txiface'},$classID,$limit->{'TrafficLimitTx'}); + tc_addtask_optimize($kernel,$config->{'rxiface'},$classID,$limit->{'TrafficLimitRx'}); } # Mark as live - $user->{'_shaper.state'} = SHAPER_LIVE; + setShaperState($lid,SHAPER_LIVE); } # Change event for tc sub do_change { - my ($kernel, $uid, $changes) = @_[KERNEL, ARG0]; + my ($kernel, $lid, $changes) = @_[KERNEL, ARG0]; # Pull in global - my $users = $globals->{'users'}; - my $user = $users->{$uid}; + my $limits = getLimits(); + my $limit = $limits->{$lid}; - $logger->log(LOG_DEBUG,"Processing changes for '$user->{'Username'}' [$uid]\n"); + $logger->log(LOG_DEBUG,"Processing changes for '$limit->{'Username'}' [$lid]\n"); # We going to pull in the defaults - my $trafficLimitTx = $user->{'TrafficLimitTx'}; - my $trafficLimitRx = $user->{'TrafficLimitRx'}; - my $trafficLimitTxBurst = $user->{'TrafficLimitTxBurst'}; - my $trafficLimitRxBurst = $user->{'TrafficLimitRxBurst'}; + my $trafficLimitTx = $limit->{'TrafficLimitTx'}; + my $trafficLimitRx = $limit->{'TrafficLimitRx'}; + my $trafficLimitTxBurst = $limit->{'TrafficLimitTxBurst'}; + my $trafficLimitRxBurst = $limit->{'TrafficLimitRxBurst'}; # Lets see if we can override them... if (defined($changes->{'TrafficLimitTx'})) { $trafficLimitTx = $changes->{'TrafficLimitTx'}; @@ -528,42 +532,45 @@ sub do_change { $trafficLimitRxBurst = $changes->{'TrafficLimitRxBurst'}; } + my $classID = getLimitAttribute($lid,'tc.class'); + + $kernel->post("_tc" => "queue" => [ '/sbin/tc','class','change', 'dev',$config->{'txiface'}, 'parent','1:2', - 'classid',"1:$user->{'tc.class'}", + 'classid',"1:$classID", 'htb', 'rate', $trafficLimitTx . "kbit", 'ceil', $trafficLimitTxBurst . "kbit", - 'prio',$user->{'TrafficPriority'}, + 'prio', $limit->{'TrafficPriority'}, ]); $kernel->post("_tc" => "queue" => [ '/sbin/tc','class','change', 'dev',$config->{'rxiface'}, 'parent','1:2', - 'classid',"1:$user->{'tc.class'}", + 'classid',"1:$classID", 'htb', 'rate', $trafficLimitRx . "kbit", 'ceil', $trafficLimitRxBurst . "kbit", - 'prio',$user->{'TrafficPriority'}, + 'prio', $limit->{'TrafficPriority'}, ]); } # Remove event for tc sub do_remove { - my ($kernel, $uid) = @_[KERNEL, ARG0]; + my ($kernel, $lid) = @_[KERNEL, ARG0]; # Pull in global - my $users = $globals->{'users'}; - my $user = $users->{$uid}; + my $limits = getLimits(); + my $limit = $limits->{$lid}; - $logger->log(LOG_DEBUG," Remove '$user->{'Username'}' [$uid]\n"); + $logger->log(LOG_DEBUG," Remove '$limit->{'Username'}' [$lid]\n"); # Grab ClassID - my $classID = $user->{'tc.class'}; - my $filterHandle = $user->{'tc.filter'}; + my $classID = getLimitAttribute($lid,'tc.class'); + my $filterHandle = getLimitAttribute($lid,'tc.filter'); # Clear up the filter $kernel->post("_tc" => "queue" => [ @@ -602,14 +609,14 @@ sub do_remove { disposeTcClass($classID); # Mark as not live - $user->{'_shaper.state'} = SHAPER_NOTLIVE; + setShaperState($lid,SHAPER_NOTLIVE); } # Function to get next available TC filter sub getTcFilter { - my $uid = shift; + my $lid = shift; my $id = pop(@{$tcFilters->{'free'}}); @@ -625,7 +632,7 @@ sub getTcFilter $id = toHex($id); } - $tcFilters->{'track'}->{$id} = $uid; + $tcFilters->{'track'}->{$id} = $lid; return $id; } @@ -644,7 +651,7 @@ sub disposeTcFilter # Function to get next available TC class sub getTcClass { - my $uid = shift; + my $lid = shift; my $id = pop(@{$tcClasses->{'free'}}); @@ -657,7 +664,7 @@ sub getTcClass $id = toHex($id); } - $tcClasses->{'track'}->{$id} = $uid; + $tcClasses->{'track'}->{$id} = $lid; return $id; } @@ -678,6 +685,17 @@ sub getUIDFromTcClass return $tcClasses->{'track'}->{$id}; } +# Get TX iface +sub getConfigTxIface +{ + return $config->{'txiface'}; +} +# Get RX iface +sub getConfigRxIface +{ + return $config->{'rxiface'}; +} + # Function to initialize an interface sub _tc_init_iface diff --git a/opentrafficshaper/plugins/tcstats/tcstats.pm b/opentrafficshaper/plugins/tcstats/tcstats.pm index 551064fec6e18fa0fe7f1aa7db06ecb4e79208fb..f9d01fe1934d7f26142f5eb495ddceea47100d43 100644 --- a/opentrafficshaper/plugins/tcstats/tcstats.pm +++ b/opentrafficshaper/plugins/tcstats/tcstats.pm @@ -128,12 +128,26 @@ sub session_tick { # Suck in global my $users = $globals->{'users'}; + my $tcConfig = $opentrafficshaper::plugins::tc::config; # Now my $now = time(); my $iface = "eth1"; + # Work out traffic direction + my $direction; + if ($iface eq opentrafficshaper::plugins::tc::getConfigTxIface()) { + $direction = 'tx'; + } elsif ($iface eq opentrafficshaper::plugins::tc::getConfigRxIface()) { + $direction = 'rx'; + } else { + # Reset tick + $kernel->delay(tick => TICK_PERIOD); + $logger->log(LOG_ERR,"[TCSTATS] Unknown interface '$iface'"); + return; + } + # TC commands to run my $cmd = [ '/sbin/tc', '-s', 'class', 'show', 'dev', $iface ]; @@ -159,16 +173,13 @@ sub session_tick { $heap->{task_data}->{$task->ID} = { 'timestamp' => $now, 'iface' => $iface, + 'direction' => $direction, 'stats' => { } }; # Build commandline string my $cmdStr = join(' ',@{$cmd}); $logger->log(LOG_DEBUG,"[TCSTATS] TASK/".$task->ID.": Starting '$cmdStr' as ".$task->ID." with PID ".$task->PID); - - - # Reset tick -# $kernel->delay(tick => TICK_PERIOD); }; @@ -179,6 +190,7 @@ sub task_child_stdout my $child = $heap->{task_by_wid}->{$task_id}; my $stats = $heap->{task_data}->{$task_id}->{'stats'}; my $iface = $heap->{task_data}->{$task_id}->{'iface'}; + my $direction = $heap->{task_data}->{$task_id}->{'direction'}; my $timestamp = $heap->{task_data}->{$task_id}->{'timestamp'}; @@ -275,6 +287,7 @@ sub task_child_stdout # Build our submission, this is basically copying the hash my %submission = %{$stats}; $submission{'timestamp'} = $timestamp; + $submission{'direction'} = $direction; $logger->log(LOG_DEBUG,"[TCSTATS] Submitting stats for [%s]",$item); $kernel->post("statistics" => "update" => $item => \%submission); diff --git a/opentrafficshaper/plugins/webserver/pages/limits.pm b/opentrafficshaper/plugins/webserver/pages/limits.pm new file mode 100644 index 0000000000000000000000000000000000000000..022ad95977fa858f6b4dc77bd84550d8fb5d1bd5 --- /dev/null +++ b/opentrafficshaper/plugins/webserver/pages/limits.pm @@ -0,0 +1,400 @@ +# OpenTrafficShaper webserver module: limits page +# Copyright (C) 2007-2013, AllWorldIT +# +# 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 3 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, see <http://www.gnu.org/licenses/>. + +package opentrafficshaper::plugins::webserver::pages::limits; + +use strict; +use warnings; + + +# Exporter stuff +require Exporter; +our (@ISA,@EXPORT,@EXPORT_OK); +@ISA = qw(Exporter); +@EXPORT = qw( +); +@EXPORT_OK = qw( +); + + +use DateTime; +use HTML::Entities; +use HTTP::Status qw( :constants ); + +use opentrafficshaper::logger; +use opentrafficshaper::plugins; +use opentrafficshaper::utils; + +use opentrafficshaper::plugins::configmanager qw( getLimits getTrafficClasses getPriorityName ); + + + +# Sidebar menu options for this module +my $menu = { + 'Limits' => { + 'Show Limits' => '', + }, + 'Admin' => { + 'Add Limit' => 'add', + }, +}; + + + +# Default page/action +sub default +{ + my ($kernel,$globals,$client_session_id,$request) = @_; + + + my $limits = getLimits(); + + # Build content + my $content = ""; + + # Header + $content .=<<EOF; +<table class="table"> + <legend>Limit List</legend> + <thead> + <tr> + <th></th> + <th>User</th> + <th>IP</th> + <th>Source</th> + <th>LastUpdate</th> + <th>Class</th> + <th>Group</th> + <th>Limits</th> + <th></th> + </tr> + </thead> + <tbody> +EOF + # Body + foreach my $uid (keys %{$limits}) { + my $limit = $limits->{$uid}; + + # Make style a bit pretty + my $style = ""; + my $icon = ""; + if ($limit->{'Status'} eq "offline") { + $icon = '<i class="glyphicon-trash"></i>'; + $style = "warning"; + } elsif ($limit->{'Status'} eq "new") { +# $icon = '<i class="glyphicon-plus"></i>'; + $style = "info"; + } elsif ($limit->{'Status'} eq "conflict") { + $icon = '<i class="glyphicon-random"></i>'; + $style = "error"; + } + + # Get a nice last update string + my $lastUpdate = DateTime->from_epoch( epoch => $limit->{'LastUpdate'} )->iso8601(); + my $limits = $limit->{'TrafficLimitTx'} . "/" . $limit->{'TrafficLimitRx'}; + + + # If the statistics plugin is loaded pull in some stats + my $statsPPSTx = my $statsRateTx = my $statsPrioTx = "unavail"; + if (plugin_is_loaded('statistics')) { + my $stats = opentrafficshaper::plugins::statistics::getLastStats($limit->{'Username'}); + # Pull off tx stats + if (my $statsTx = $stats->{'tx'}) { + $statsPPSTx = $statsTx->{'current_pps'}; + $statsRateTx = $statsTx->{'current_rate'}; + $statsPrioTx = getPriorityName($limit->{'TrafficPriority'}); + } + } + + $content .= <<EOF; + <tr class="$style"> + <td>$icon</td> + <td class="limit"> + $limit->{'Username'} + <span class="limit-data" style="display:none"> + <table width="100%" border="0"> + <tr> + <td>Tx Priority</td> + <td>$statsPrioTx</td> + <td> </td> + <td>Tx Priority</td> + <td>-</td> + </tr> + <tr> + <td>Tx PPS</td> + <td>$statsPPSTx</td> + <td> </td> + <td>Rx PPS</td> + <td>-</td> + </tr> + <tr> + <td>Tx Rate</td> + <td>$statsRateTx</td> + <td> </td> + <td>Rx Rate</td> + <td>-</td> + </tr> + </table> + </span> + </td> + <td>$limit->{'IP'}</td> + <td>$limit->{'Source'}</td> + <td>$lastUpdate</td> + <td>$limit->{'ClassID'}</td> + <td>$limit->{'GroupID'}</td> + <td>$limits</td> + <td> + <i class="glyphicon glyphicon-wrench"></i> + <i class="glyphicon glyphicon-stats"></i> + </td> + </tr> +EOF + } + # No results + if (keys %{$limits} < 1) { + $content .=<<EOF; + <tr class="info"> + <td colspan="8"><p class="text-center">No Results</p></td> + </tr> +EOF + } + + # Footer + $content .=<<EOF; + </tbody> +</table> +EOF + + my $style = <<EOF; + .popover { + max-width:none; + } + + .popover td:nth-child(4), .popover td:first-child { + font-weight:bold; + text-transform:capitalize; + } +EOF + + my $javascript = <<EOF; + \$(document).ready(function(){ + \$('.limit').each(function(){ + \$(this).popover({ + html: true, + content: \$(this).find('.limit-data').html(), + placement: 'bottom', + trigger: 'hover', + container: \$(this), + title: 'Statistics', + }); + }) + }); +EOF + + return (HTTP_OK,$content,{ 'style' => $style, 'menu' => $menu, 'javascript' => $javascript }); +} + + +# Add action +sub add +{ + my ($kernel,$globals,$client_session_id,$request) = @_; + + + # Setup our environment + my $logger = $globals->{'logger'}; + + # Errors to display + my @errors; + # Form items + my $params = { + 'inputUsername' => undef, + 'inputIP' => undef, + 'inputTrafficClass' => undef, + 'inputExpires' => undef, + 'inputExpiresModifier' => undef, + 'inputLimitTx' => undef, + 'inputLimitRx' => undef, + }; + + # If this is a form try parse it + if ($request->method eq "POST") { + # Parse form data + $params = parseFormContent($request->content); + + # If user pressed cancel, redirect + if (defined($params->{'cancel'})) { + # Redirects to default page + return (HTTP_TEMPORARY_REDIRECT,'limits'); + } + + # Check POST data + my $username; + if (!defined($username = isUsername($params->{'inputUsername'}))) { + push(@errors,"Username is not valid"); + } + my $ipAddress; + if (!defined($ipAddress = isIP($params->{'inputIP'}))) { + push(@errors,"IP address is not valid"); + } + my $trafficLimitTx; + if (!defined($trafficLimitTx = isNumber($params->{'inputLimitTx'}))) { + push(@errors,"Download limit is not valid"); + } + my $trafficLimitRx; + if (!defined($trafficLimitRx = isNumber($params->{'inputLimitRx'}))) { + push(@errors,"Upload limit is not valid"); + } + + # If there are no errors we need to push this update + if (!@errors) { + # Build limit + my $limit = { + 'Username' => $username, + 'IP' => $ipAddress, + 'GroupID' => 1, + 'ClassID' => 1, + 'TrafficLimitTx' => $trafficLimitTx, + 'TrafficLimitRx' => $trafficLimitRx, + 'TrafficLimitTxBurst' => $trafficLimitTx, + 'TrafficLimitRxBurst' => $trafficLimitRx, + 'Status' => "online", + 'Source' => "plugin.webserver.limits", + }; + + # Throw the change at the config manager + $kernel->post("configmanager" => "process_change" => $limit); + + $logger->log(LOG_INFO,"[WEBSERVER/LIMIS/ADD] User: $username, IP: $ipAddress, Group: 1, Class: 2, ". + "Limits: ".prettyUndef($trafficLimitTx)."/".prettyUndef($trafficLimitRx).", Burst: ".prettyUndef($trafficLimitTx)."/".prettyUndef($trafficLimitRx)); + + return (HTTP_TEMPORARY_REDIRECT,'limits'); + } + } + + # Sanitize params if we need to + foreach my $item (keys %{$params}) { + $params->{$item} = defined($params->{$item}) ? encode_entities($params->{$item}) : ""; + } + + # Build content + my $content = ""; + + # Form header + $content .=<<EOF; +<form role="form" method="post"> + <legend>Add Manual Limit</legend> +EOF + + # Spit out errors if we have any + if (@errors > 0) { + foreach my $error (@errors) { + $content .= '<div class="alert alert-error">'.$error.'</div>'; + } + } + + # Generate traffic class list + my $trafficClasses = getTrafficClasses(); + my $trafficClassStr = ""; + foreach my $classID (keys %{$trafficClasses}) { + $trafficClassStr .= '<option value="'.$classID.'">'.$trafficClasses->{$classID}.'</option>'; + } + + # Header + $content .=<<EOF; + <div class="form-group"> + <label for="inputUsername" class="col-lg-2 control-label">Username</label> + <div class="row"> + <div class="col-lg-4"> + <input name="inputUsername" type="text" placeholder="Username" class="form-control" value="$params->{'inputUsername'}" /> + </div> + </div> + </div> + <div class="form-group"> + <label for="inputIP" class="col-lg-2 control-label">IP Address</label> + <div class="row"> + <div class="col-lg-4"> + <input name="inputIP" type="text" placeholder="IP Address" class="form-control" value="$params->{'inputIP'}" /> + </div> + </div> + </div> + <div class="form-group"> + <label for="inputTafficClass" class="col-lg-2 control-label">Traffic Class</label> + <div class="row"> + <div class="col-lg-2"> + <select name="inputTrafficClass" placeholder="Traffic Class" class="form-control" value="$params->{'inputTrafficClass'}"> + $trafficClassStr + </select> + </div> + </div> + </div> + <div class="form-group"> + <label for="inputExpires" class="col-lg-2 control-label">Expires</label> + <div class="row"> + <div class="col-lg-2"> + <input name="inputExpires" type="text" placeholder="Expires" class="form-control" value="$params->{'inputExpires'}" /> + </div> + <div class="col-lg-2"> + <select name="inputExpiresModifier" placeholder="Expires Modifier" class="form-control" value="$params->{'inputExpiresModifier'}"> + <option value="m">Mins</option> + <option value="h">Hours</option> + <option value="d">Days</option> + </select> + </div> + </div> + </div> + <div class="form-group"> + <label for="inputLimitTx" class="col-lg-2 control-label">Download Limit</label> + <div class="row"> + <div class="col-lg-3"> + <div class="input-group"> + <input name="inputLimitTx" type="text" placeholder="TX Limit" class="form-control" value="$params->{'inputLimitTx'}" /> + <span class="input-group-addon">Kbps<span> + </div> + </div> + </div> + </div> + <div class="form-group"> + <label for="inputLimitRx" class="col-lg-2 control-label">Upload Limit</label> + <div class="row"> + <div class="col-lg-3"> + <div class="input-group"> + <input name="inputLimitRx" type="text" placeholder="RX Limit" class="form-control" value="$params->{'inputLimitRx'}" /> + <span class="input-group-addon">Kbps<span> + </div> + </div> + </div> + </div> + <div class="form-group"> + <label for="inputDescription" class="col-lg-2 control-label">Description</label> + <div class="row"> + <div class="col-lg-4"> + <textarea name="inputDescription" placeholder="Description" rows="3" class="form-control"></textarea> + </div> + </div> + </div> + <div class="form-group"> + <button type="submit" class="btn btn-primary">Add</button> + <button name="cancel" type="submit" class="btn">Cancel</button> + </div> +</form> +EOF + + return (HTTP_OK,$content,{ 'menu' => $menu }); +} + + +1; diff --git a/opentrafficshaper/plugins/webserver/pages/statistics.pm b/opentrafficshaper/plugins/webserver/pages/statistics.pm new file mode 100644 index 0000000000000000000000000000000000000000..349673f43ccd0bb6d988ac611e74e064b42ef82c --- /dev/null +++ b/opentrafficshaper/plugins/webserver/pages/statistics.pm @@ -0,0 +1,200 @@ +# OpenTrafficShaper webserver module: users page +# Copyright (C) 2007-2013, AllWorldIT +# +# 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 3 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, see <http://www.gnu.org/licenses/>. + +package opentrafficshaper::plugins::webserver::pages::statistics; + +use strict; +use warnings; + + +# Exporter stuff +require Exporter; +our (@ISA,@EXPORT,@EXPORT_OK); +@ISA = qw(Exporter); +@EXPORT = qw( +); +@EXPORT_OK = qw( +); + + +use DateTime; +use HTML::Entities; +use HTTP::Status qw( :constants ); + +use opentrafficshaper::logger; +use opentrafficshaper::utils; + +use opentrafficshaper::plugins::statistics::statistics; + +# Default page/action +sub default +{ + my ($kernel,$globals,$client_session_id,$request) = @_; + + + # Build content + my $content = <<EOF; + + <div id="header"> + <h2>Traffic Shaper Stats</h2> + </div> + + <div id="content" style="float:left"> + + <div style="position: relative; top:50px;"> + <h4 style="color:#8f8f8f;">Json Data (loads from /static/stats.json.js)</h4> + <br/> + + + <div id="ajaxData" class="ajaxData" style="float:left; width:1024px; height: 560px"></div> + </div> + + </div> + +EOF + + # FIXME - Dynamic script inclusion required + + #$content .= statistics::do_test(); +# $content .= opentrafficshaper::plugins::statistics::do_test(); + + # Files loaded at end of HTML document + my @javascripts = ( + '/static/awit-flot/jquery.flot.min.js', + '/static/awit-flot/jquery.flot.time.min.js', +# '/static/awit-flot/jquery.flot.websockets.js' + ); + + # String put in <script> </script> tags after the above files are loaded + my $javascript =<<EOF; + // + // Tooltip - Displays detailed information regarding the data point + // + function showTooltip(x, y, contents) { + jQuery('<div id="tooltip">' + contents + '</div>').css( { + position: 'absolute', + display: 'none', + top: y - 30, + left: x - 50, + color: "#fff", + padding: '2px 5px', + 'border-radius': '6px', + 'background-color': '#000', + opacity: 0.80 + }).appendTo("body").fadeIn(200); + } + + var previousPoint = null; + + jQuery("#ajaxData").bind("plothover", function (event, pos, item) { + if (item) { + if (previousPoint != item.dataIndex) { + previousPoint = item.dataIndex; + + jQuery("#tooltip").remove(); + var x = item.datapoint[0].toFixed(0), + y = item.datapoint[1].toFixed(0); + + showTooltip(item.pageX, item.pageY, + item.series.label + ' date: ' + month); + } + } + else { + jQuery("#tooltip").remove(); + previousPoint = null; + } + }); + + + // Setting up the graph here + options = { + series: { + lines: { show: true, + lineWidth: 1, + fill: true, + fillColor: { colors: [ { opacity: 0.1 }, { opacity: 0.13 } ] } + }, + points: { show: true, + lineWidth: 2, + radius: 3 + }, + shadowSize: 0, + stack: true + }, + grid: { + hoverable: true, + clickable: false, + tickColor: "#f9f9f9", + borderWidth: 0 + }, + legend: { + // show: false + labelBoxBorderColor: "#fff" + //,container: '#legend-container' + }, + + xaxis: { + mode: "time", + tickSize: [2, "second"], + tickFormatter: function (v, axis) { + var date = new Date(v); + + if (date.getSeconds() % 1 == 0) { + var hours = date.getHours() < 10 ? "0" + date.getHours() : date.getHours(); + var minutes = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes(); + var seconds = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds(); + + return hours + ":" + minutes + ":" + seconds; + } else { + return ""; + } + }, + }, + yaxis: { + min: 0, + max: 4000, + tickFormatter: function (v, axis) { + if (v % 10 == 0) { + return v; + } else { + return ""; + } + }, + } + } + //*/ + + // loading the stats.json.js file as data and drawing the graph on successful response. + jQuery.ajax({ + url: '/static/stats.json.js', + dataType: "json", + success: function(statsData){ + plot = null; + if (statsData.length > 0) { + plot = jQuery.plot(jQuery("#ajaxData"), statsData, options); + } + } + }); + + +EOF + + return (HTTP_OK,$content,{ 'javascripts' => \@javascripts, 'javascript' => $javascript }); +} + + + +1; diff --git a/opentrafficshaper/plugins/webserver/pages/users.pm b/opentrafficshaper/plugins/webserver/pages/users.pm deleted file mode 100644 index 466fc7f69fa9a3edada31bb5373d4fa45adedb8e..0000000000000000000000000000000000000000 --- a/opentrafficshaper/plugins/webserver/pages/users.pm +++ /dev/null @@ -1,278 +0,0 @@ -# OpenTrafficShaper webserver module: users page -# Copyright (C) 2007-2013, AllWorldIT -# -# 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 3 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, see <http://www.gnu.org/licenses/>. - -package opentrafficshaper::plugins::webserver::pages::users; - -use strict; -use warnings; - - -# Exporter stuff -require Exporter; -our (@ISA,@EXPORT,@EXPORT_OK); -@ISA = qw(Exporter); -@EXPORT = qw( -); -@EXPORT_OK = qw( -); - - -use DateTime; -use HTML::Entities; -use HTTP::Status qw( :constants ); - -use opentrafficshaper::logger; -use opentrafficshaper::utils; - - - -# Sidebar menu options for this module -my $menu = { - 'Users' => { - 'Show Users' => '', - }, - 'Admin' => { - 'Add User' => 'add', - }, -}; - - - -# Default page/action -sub default -{ - my ($kernel,$globals,$client_session_id,$request) = @_; - - - my $users = $globals->{'users'}; - - # Build content - my $content = ""; - - # Header - $content .=<<EOF; -<table class="table"> - <legend>User List</legend> - <thead> - <tr> - <th>#</th> - <th>User</th> - <th>IP</th> - <th>Source</th> - <th>LastUpdate</th> - <th>Class</th> - <th>Group</th> - <th>Limits</th> - </tr> - </thead> - <tbody> -EOF - # Body - foreach my $uid (keys %{$users}) { - my $user = $users->{$uid}; - - # Make style a bit pretty - my $style = ""; - my $icon = ""; - if ($user->{'Status'} eq "offline") { - $icon = '<i class="icon-trash"></i>'; - $style = "warning"; - } elsif ($user->{'Status'} eq "new") { -# $icon = '<i class="icon-plus"></i>'; - $style = "info"; - } elsif ($user->{'Status'} eq "conflict") { - $icon = '<i class="icon-random"></i>'; - $style = "error"; - } - - # Get a nice last update string - my $lastUpdate = DateTime->from_epoch( epoch => $user->{'LastUpdate'} )->iso8601(); - my $limits = $user->{'TrafficLimitTx'} . "/" . $user->{'TrafficLimitRx'}; - - $content .=<<EOF; - <tr class="$style"> - <td>$icon</td> - <td>$user->{'Username'}</td> - <td>$user->{'IP'}</td> - <td>$user->{'Source'}</td> - <td>$lastUpdate</td> - <td>$user->{'ClassID'}</td> - <td>$user->{'GroupID'}</td> - <td>$limits</td> - </tr> -EOF - } - # No results - if (keys %{$globals->{'users'}} < 1) { - $content .=<<EOF; - <tr class="info"> - <td colspan="8"><p class="text-center">No Results</p></td> - </tr> -EOF - } - - # Footer - $content .=<<EOF; - </tbody> -</table> -EOF - - - return (HTTP_OK,$content,$menu); -} - - -# Add action -sub add -{ - my ($kernel,$globals,$client_session_id,$request) = @_; - - - # Setup our environment - my $logger = $globals->{'logger'}; - - # Errors to display - my @errors; - # Form items - my $params = { - 'inputUsername' => undef, - 'inputIP' => undef, - 'inputLimitTx' => undef, - 'inputLimitRx' => undef, - }; - - # If this is a form try parse it - if ($request->method eq "POST") { - # Parse form data - $params = parseFormContent($request->content); - - # If user pressed cancel, redirect - if (defined($params->{'cancel'})) { - # Redirects to default page - return (HTTP_TEMPORARY_REDIRECT,'users'); - } - - # Check POST data - my $username; - if (!defined($username = isUsername($params->{'inputUsername'}))) { - push(@errors,"Username is not valid"); - } - my $ipAddress; - if (!defined($ipAddress = isIP($params->{'inputIP'}))) { - push(@errors,"IP address is not valid"); - } - my $trafficLimitTx; - if (!defined($trafficLimitTx = isNumber($params->{'inputLimitTx'}))) { - push(@errors,"Download limit is not valid"); - } - my $trafficLimitRx; - if (!defined($trafficLimitRx = isNumber($params->{'inputLimitRx'}))) { - push(@errors,"Upload limit is not valid"); - } - - # If there are no errors we need to push this update - if (!@errors) { - # Build user - my $user = { - 'Username' => $username, - 'IP' => $ipAddress, - 'GroupID' => 1, - 'ClassID' => 1, - 'TrafficLimitTx' => $trafficLimitTx, - 'TrafficLimitRx' => $trafficLimitRx, - 'TrafficLimitTxBurst' => $trafficLimitTx, - 'TrafficLimitRxBurst' => $trafficLimitRx, - 'Status' => "online", - 'Source' => "plugin.webserver.users", - }; - - # Throw the change at the config manager - $kernel->post("configmanager" => "process_change" => $user); - - $logger->log(LOG_INFO,"[WEBSERVER/USERS/ADD] User: $username, IP: $ipAddress, Group: 1, Class: 2, ". - "Limits: ".prettyUndef($trafficLimitTx)."/".prettyUndef($trafficLimitRx).", Burst: ".prettyUndef($trafficLimitTx)."/".prettyUndef($trafficLimitRx)); - - return (HTTP_TEMPORARY_REDIRECT,'users'); - } - } - - # Sanitize params if we need to - foreach my $item (keys %{$params}) { - $params->{$item} = defined($params->{$item}) ? encode_entities($params->{$item}) : ""; - } - - # Build content - my $content = ""; - - # Form header - $content .=<<EOF; -<form class="form-horizontal" method="post"> - <legend>Add Manual User</legend> -EOF - - # Spit out errors if we have any - if (@errors > 0) { - foreach my $error (@errors) { - $content .= '<div class="alert alert-error">'.$error.'</div>'; - } - } - - # Header - $content .=<<EOF; - <div class="control-group"> - <label class="control-label" for="inputUsername">Username</label> - <div class="controls"> - <input name="inputUsername" type="text" placeholder="Username" value="$params->{'inputUsername'}" /> - </div> - </div> - <div class="control-group"> - <label class="control-label" for="inputIP">IP Address</label> - <div class="controls"> - <input name="inputIP" type="text" placeholder="IP Address" value="$params->{'inputIP'}" /> - </div> - </div> - <div class="control-group"> - <label class="control-label" for="inputLimitTx">Download Limit</label> - <div class="controls"> - <div class="input-append"> - <input name="inputLimitTx" type="text" class="span5" placeholder="TX Limit" value="$params->{'inputLimitTx'}" /> - <span class="add-on">Kbps<span> - </div> - </div> - </div> - <div class="control-group"> - <label class="control-label" for="inputLimitRx">Upload Limit</label> - <div class="controls"> - <div class="input-append"> - <input name="inputLimitRx" type="text" class="span5" placeholder="RX Limit" value="$params->{'inputLimitRx'}" /> - <span class="add-on">Kbps<span> - </div> - </div> - </div> - <div class="control-group"> - <div class="controls"> - <button type="submit" class="btn btn-primary">Add</button> - <button name="cancel" type="submit" class="btn">Cancel</button> - </div> - </div> -</form> -EOF - - return (200,$content,$menu); -} - - -1; diff --git a/opentrafficshaper/plugins/webserver/snapins/websockets/statistics/statistics.pm b/opentrafficshaper/plugins/webserver/snapins/websockets/statistics/statistics.pm index 6d7abc4429d6489af20dd3c0bd34b16a5943bd54..7acbf8b6d46a93d24bdeb5edf82cc3a9bbc838be 100644 --- a/opentrafficshaper/plugins/webserver/snapins/websockets/statistics/statistics.pm +++ b/opentrafficshaper/plugins/webserver/snapins/websockets/statistics/statistics.pm @@ -113,7 +113,7 @@ sub session_init } -# Signal that the client has connected +# Send data to client sub do_send { my ($kernel,$heap,$uid,$data) = @_[KERNEL, HEAP, ARG0, ARG1]; @@ -125,6 +125,8 @@ sub do_send use Data::Dumper; print STDERR "Got request to send client '$client_session_id': ".Dumper($data); +# my $json = sprintf('{"label": "%s", data: [%s] }', ); + $socket->put("hello there"); } } diff --git a/opentrafficshaper/plugins/webserver/webserver.pm b/opentrafficshaper/plugins/webserver/webserver.pm index 378fac70f5535cc004e3499cef691ba3ff9faddf..83e22c429b4ef3eefaa911f889cf934cc5dac513 100644 --- a/opentrafficshaper/plugins/webserver/webserver.pm +++ b/opentrafficshaper/plugins/webserver/webserver.pm @@ -35,7 +35,8 @@ use opentrafficshaper::plugins; # Pages (this is used a little below) use opentrafficshaper::plugins::webserver::pages::static; use opentrafficshaper::plugins::webserver::pages::index; -use opentrafficshaper::plugins::webserver::pages::users; +use opentrafficshaper::plugins::webserver::pages::limits; +use opentrafficshaper::plugins::webserver::pages::statistics; # Exporter stuff @@ -84,9 +85,12 @@ my $resources = { 'static' => { '_catchall' => \&opentrafficshaper::plugins::webserver::pages::static::_catchall, }, - 'users' => { - 'default' => \&opentrafficshaper::plugins::webserver::pages::users::default, - 'add' => \&opentrafficshaper::plugins::webserver::pages::users::add, + 'limits' => { + 'default' => \&opentrafficshaper::plugins::webserver::pages::limits::default, + 'add' => \&opentrafficshaper::plugins::webserver::pages::limits::add, + }, + 'statistics' => { + 'default' => \&opentrafficshaper::plugins::webserver::pages::statistics::default, }, }, }; @@ -280,7 +284,7 @@ sub httpRedirect # Create a response object sub httpCreateResponse { - my ($module,$content,$menu) = @_; + my ($module,$content,$options) = @_; # Throw out message to client to authenticate first @@ -289,41 +293,58 @@ sub httpCreateResponse # Check if we have a menu structure, if we do, display the sidebar + my $styleStr = ""; my $menuStr = ""; - if (defined($menu)) { - $menuStr =<<EOF; - <div class="span2"> - <div class="well sidebar-nav"> - <ul class="nav nav-list"> + my $javascriptStr = ""; + if (defined($options)) { + # Check if style snippet exists + if (defined(my $style = $options->{'style'})) { + $styleStr .= $style; + } + + # Check if menu exists + if (my $menu = $options->{'menu'}) { + $menuStr =<<EOF; + <ul class="nav nav-pills nav-stacked"> EOF - # Loop with sub menu sections - foreach my $section (keys %{$menu}) { -# <li class="nav-header">Sidebar</li> -# <li class="active"><a href="#">Link</a></li> -# <li><a href="#">Link</a></li> -# <li class="nav-header">Sidebar</li> -# <li><a href="#">Link</a></li> - # Loop with menu items - foreach my $item (keys %{$menu->{$section}}) { - my $link = "/" . $module . "/" . $menu->{$section}->{$item}; - # Sanitize slightly - $link =~ s,/+$,,; - - # Build sections - $menuStr .=<<EOF; + # Loop with sub menu sections + foreach my $section (keys %{$menu}) { + # Loop with menu items + foreach my $item (keys %{$menu->{$section}}) { + my $link = "/" . $module . "/" . $menu->{$section}->{$item}; + # Sanitize slightly + $link =~ s,/+$,,; + + # Build sections + $menuStr .=<<EOF; <li class="nav-header">$section</li> <li><a href="$link">$item</a></li> EOF + } } + $menuStr .=<<EOF; + </ul> +EOF } - $menuStr .=<<EOF; - </ul> - </div><!--/.well --> - </div><!--/span--> + + # Check if we have a list of javascript assets + if (defined(my $javascripts = $options->{'javascripts'})) { + foreach my $script (@{$javascripts}) { + $javascriptStr .=<<EOF; + <script type="text/javascript" src="$script"></script> EOF + } + } + # Check if javascript snippet exists + if (defined(my $javascript = $options->{'javascript'})) { + $javascriptStr .=<<EOF; + <script type="text/javascript"> +$javascript + </script> +EOF + } } - # Build action response my $resp = HTTP::Response->new( HTTP_OK,"Ok", @@ -335,72 +356,68 @@ EOF <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- Assets --> <link href="/static/favicon.ico" rel="icon" /> - <link href="/static/jquery-ui/css/ui-lightness/jquery-ui.min.css" rel="stylesheet" media="screen"> - <link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen"> + <link href="/static/jquery-ui/css/ui-lightness/jquery-ui.min.css" rel="stylesheet"> + <link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <style type="text/css"> body { - padding-top: 60px; - padding-bottom: 40px; + padding-top: 50px; } - .sidebar-nav { - padding: 9px 0; - } - \@media (max-width: 980px) { - /* Enable use of floated navbar text */ - .navbar-text.pull-right { - float: none; - padding-left: 5px; - padding-right: 5px; - } + + .main-area { + padding-top: 15px; + padding-bottom: 15px; } +$styleStr </style> - <!-- End Assets --> - <link href="/static/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet" media="screen"> </head> - <body> + <body> <div class="navbar navbar-inverse navbar-fixed-top"> - <div class="navbar-inner"> - <div class="container-fluid"> - <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"> + <div class="container"> + <div class="navbar-header"> + <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> - <a class="brand" href="/">OpenTrafficShaper</a> - <div class="nav-collapse collapse"> - <p class="navbar-text pull-right">Logged in as <a href="#" class="navbar-link">Username</a> </p> - <ul class="nav"> - <li class="active"><a href="#">Home</a></li> - <li><a href="/users">Users</a></li> - </ul> - </div><!--/.nav-collapse --> + <a class="navbar-brand" href="/">OpenTrafficShaper</a> + </div> + <div class="collapse navbar-collapse"> + <ul class="nav navbar-nav"> + <li class="active"><a href="#">Home</a></li> + <li><a href="/limits">Limits</a></li> + </ul> </div> </div> </div> - <div class="container-fluid"> - <div class="row-fluid"> - $menuStr - <div class="span10"> - $content - </div><!--/span--> - </div><!--/row--> + <div class="main-area container"> + <div class="col-md-2"> +$menuStr + </div> + <div class="col-md-10"> +$content + </div> + </div> <hr> <footer> <p class="muted">v$globals->{'version'} - Copyright © 2013, <a href="http://www.allworldit.com">AllWorldIT</a></p> </footer> - </div><!--/.fluid-container--> + </div> + + </body> + + + <script type="text/javascript" src="/static/jquery/js/jquery.min.js"></script> + <script type="text/javascript" src="/static/jquery-ui/js/jquery-ui.min.js"></script> + <script type="text/javascript" src="/static/bootstrap/js/bootstrap.min.js"></script> +$javascriptStr - <!-- Javascript --> - <script src="/static/jquery/js/jquery.min.js"></script> - <script src="/static/jquery-ui/js/jquery-ui.min.js"></script> - <script src="/static/bootstrap/js/bootstrap.min.js"></script> - </body> </html> EOF + ## FIXME - Dyanmic script inclusion required return $resp; } @@ -551,11 +568,11 @@ sub _parse_http_resource my (undef,$dmodule,$daction) = $request->uri->path_segments(); # If any is blank, set it to the default $dmodule = "index" if (!defined($dmodule) || $dmodule eq ""); - $daction = "default" if (!defined($daction)); + $daction = "default" if (!defined($daction) || $daction eq ""); # Sanitize (my $module = $dmodule) =~ s/[^A-Za-z0-9]//g; (my $action = $daction) =~ s/[^A-Za-z0-9]//g; - +print STDERR "module = $module, action = $action\n"; # If module name is sneaky? then just block it if ($module ne $dmodule) { return httpDisplayFault(HTTP_FORBIDDEN,"Method Not Allowed","The requested resource '$module' is not allowed.");