Commit 803ff4bb authored by Nigel Kukard's avatar Nigel Kukard

Major code refactoring

* Support for interface groups
* Support for class CIR/Limit per interface
* Match priority support
* Totally dynamic TC class support per interface
* Totally dynamic TC filter support per interface per match priority
* Interface statistics support
parent 1556f7d8
......@@ -46,9 +46,13 @@ load=webserver/snapins/websockets/statistics
# Group 1 is by default the "Default" group
group=1:Default
# Traffic classes
# ID's and short description of traffic classes to Setup. Traffic is
# priortized as the lowest number getting the highest priority
#
# The second parameter is the name of the class
#
class=1:High Priority
class=2:Platinum
class=3:Gold
......@@ -58,18 +62,50 @@ class=6:Best Effort
# Default pool
# For traffic not classified, we can send it to a rate-limited pool
#
# If "yes" is specified traffic class 99 will be used, if a number is specified then that traffic classes
# will be used instead.
# For traffic not classified, we can send it to a specific traffic class
#
# Defaults to "no"
#use_default_pool=no
#default_pool=no
# Interface group
# This is a friendly name for a group of interfaces used for TX & RX
# Its in the format of txiface,rxiface:Friendly name
# The txiface is always the interface the client traffic is transmitted on (downloaded)
# The rxiface is always the interface the client traffic is received on (uploaded)
interface_group=eth1,eth0:LAN-side
#
# Interface setup
#
# Default pool TX rate, must be specified if using a default pool
#default_pool_txrate=4096
# Same with RX rate
#default_pool_rxrate=4096
[shaping.interface eth0]
# This is the friendly name used when displaying this interface
name=WAN interface
# The rate is specified in Kbps
rate=100000
# Class format is: ClassID:CIR/Limit
# If Limit is not specified it defaults to CIR
# if the class definition is omitted, defaults to rate of interface
# The CIR and Limit are specified in Kbps or percentage
class_rate=1:10
class_rate=2:5%/5%
class_rate=3:5%
class_rate=4:5/10
class_rate=5:5%
class_rate=6:5%
[shaping.interface eth1]
name=LAN Interface
rate=100000
class_rate=1:10
class_rate=2:5%/5%
class_rate=3:5%
class_rate=4:5/10
class_rate=5:5%
#
......
This diff is collapsed.
# OpenTrafficShaper radius module
# 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
......@@ -55,7 +55,7 @@ use constant {
our $pluginInfo = {
Name => "Radius",
Version => VERSION,
Init => \&plugin_init,
Start => \&plugin_start,
};
......@@ -66,7 +66,9 @@ my $globals;
my $logger;
# Our own data storage
my $config = {
'expiry_period' => DEFAULT_EXPIRY_PERIOD
'expiry_period' => DEFAULT_EXPIRY_PERIOD,
'interface_group' => 'eth1,eth0',
'match_priority' => 2,
};
my $dictionary;
......@@ -84,7 +86,7 @@ sub plugin_init
$logger->log(LOG_NOTICE,"[RADIUS] OpenTrafficShaper Radius Module v".VERSION." - Copyright (c) 2013, AllWorldIT");
# Split off dictionaries to load
my @dicts = ref($globals->{'file.config'}->{'plugin.radius'}->{'dictionary'}) eq "ARRAY" ?
my @dicts = ref($globals->{'file.config'}->{'plugin.radius'}->{'dictionary'}) eq "ARRAY" ?
@{$globals->{'file.config'}->{'plugin.radius'}->{'dictionary'}} : ( $globals->{'file.config'}->{'plugin.radius'}->{'dictionary'} );
foreach my $dict (@dicts) {
$dict =~ s/\s+//g;
......@@ -118,6 +120,32 @@ sub plugin_init
$config->{'expiry_period'} = $expiry;
}
# Default interface group to use
if (defined(my $interfaceGroup = $globals->{'file.config'}->{'plugin.radius'}->{'interface_group'})) {
if (isInterfaceGroupIsValid($interfaceGroup)) {
$logger->log(LOG_INFO,"[RADIUS] Set interface_group to '$interfaceGroup'");
$config->{'interface_group'} = $interfaceGroup;
} else {
$logger->log(LOG_WARN,"[RADIUS] Cannot set 'interface_group' as value '$interfaceGroup' is invalid");
}
}
# Default match priority to use
if (defined(my $matchPriority = $globals->{'file.config'}->{'plugin.radius'}->{'match_priority'})) {
if (isInterfaceGroupIsValid($matchPriority)) {
$logger->log(LOG_INFO,"[RADIUS] Set match_priority to '$matchPriority'");
$config->{'match_priority'} = $matchPriority;
} else {
$logger->log(LOG_WARN,"[RADIUS] Cannot set 'match_priority' as value '$matchPriority' is invalid");
}
}
# Check if we must override the expiry time
if (defined(my $expiry = $globals->{'file.config'}->{'plugin.radius'}->{'expiry_period'})) {
$logger->log(LOG_INFO,"[RADIUS] Set expiry_period to '$expiry'");
$config->{'expiry_period'} = $expiry;
}
# Radius listener
POE::Session->create(
inline_states => {
......@@ -283,13 +311,15 @@ sub session_read
my $user = {
'Username' => $username,
'IP' => $pkt->attr('Framed-IP-Address'),
'InterfaceGroupID' => $config->{'interface_group'},
'MatchPriorityID' => $config->{'match_priority'},
'GroupID' => $trafficGroup,
'ClassID' => $trafficClass,
'TrafficLimitTx' => $trafficLimitTx,
'TrafficLimitRx' => $trafficLimitRx,
'TrafficLimitTxBurst' => $trafficLimitTxBurst,
'TrafficLimitRxBurst' => $trafficLimitRxBurst,
'Expires' => $now + (defined($globals->{'file.config'}->{'plugin.radius'}->{'expire_entries'}) ?
'Expires' => $now + (defined($globals->{'file.config'}->{'plugin.radius'}->{'expire_entries'}) ?
$globals->{'file.config'}->{'plugin.radius'}->{'expire_entries'} : $config->{'expiry_period'}),
'Status' => getStatus($pkt->rawattr('Acct-Status-Type')),
'Source' => "plugin.radius",
......@@ -298,8 +328,10 @@ sub session_read
# Throw the change at the config manager
$kernel->post("configmanager" => "process_limit_change" => $user);
$logger->log(LOG_INFO,"[RADIUS] Code: $user->{'Status'}, User: $user->{'Username'}, IP: $user->{'IP'}, Group: $user->{'GroupID'}, Class: $user->{'ClassID'}, ".
"CIR: ".prettyUndef($trafficLimitTx)."/".prettyUndef($trafficLimitRx).", Limit: ".prettyUndef($trafficLimitTxBurst)."/".prettyUndef($trafficLimitRxBurst));
$logger->log(LOG_INFO,"[RADIUS] Code: %s, User: %s, IP: %s, InterfaceGroup: %s, MatchPriorityID: %s, Group: %s, Class: %s, CIR: %s/%s, Limit: %s/%s",
$user->{'Status'}, $user->{'Username'}, $user->{'IP'}, $user->{'InterfaceGroupID'}, $user->{'MatchPriorityID'}, $user->{'GroupID'},
$user->{'ClassID'}, prettyUndef($trafficLimitTx), prettyUndef($trafficLimitRx), prettyUndef($trafficLimitTxBurst), prettyUndef($trafficLimitRxBurst)
);
}
......
This diff is collapsed.
......@@ -30,6 +30,9 @@ use opentrafficshaper::constants;
use opentrafficshaper::logger;
use opentrafficshaper::utils;
use opentrafficshaper::plugins::configmanager qw(
getInterfaces
);
# Exporter stuff
......@@ -152,31 +155,18 @@ sub session_tick
my $now = time();
# Loop with interfaces that need stats
my $ifaces = 0;
foreach my $iface (opentrafficshaper::plugins::tc::getInterfaces()) {
my $interfaceCount = 0;
foreach my $interface (@{getInterfaces()}) {
# Skip to next if we've already run for this iface
if (defined($lastStats->{$iface}) && $lastStats->{$iface} + opentrafficshaper::plugins::statistics::STATISTICS_PERIOD > $now) {
# Skip to next if we've already run for this interface
if (defined($lastStats->{$interface}) && $lastStats->{$interface} + opentrafficshaper::plugins::statistics::STATISTICS_PERIOD > $now) {
next;
}
# Work out traffic direction
my $direction;
if ($iface eq opentrafficshaper::plugins::tc::getConfigTxIface()) {
$direction = opentrafficshaper::plugins::statistics::STATISTICS_DIR_TX;
} elsif ($iface eq opentrafficshaper::plugins::tc::getConfigRxIface()) {
$direction = opentrafficshaper::plugins::statistics::STATISTICS_DIR_RX;
} else {
# Reset tick
$kernel->delay(tick => TICK_PERIOD);
$logger->log(LOG_ERR,"[TCSTATS] Unknown interface '$iface'");
next;
}
$logger->log(LOG_INFO,"[TCSTATS] Generating stats for '$iface'");
$logger->log(LOG_INFO,"[TCSTATS] Generating stats for '$interface'");
# TC commands to run
my $cmd = [ '/sbin/tc', '-s', 'class', 'show', 'dev', $iface, 'parent', '1:' ];
my $cmd = [ '/sbin/tc', '-s', 'class', 'show', 'dev', $interface, 'parent', '1:' ];
# Create task
my $task = POE::Wheel::Run->new(
......@@ -199,8 +189,7 @@ sub session_tick
# Signal events include the process ID.
$heap->{task_data}->{$task->ID} = {
'timestamp' => $now,
'iface' => $iface,
'direction' => $direction,
'interface' => $interface,
'current_stat' => { }
};
......@@ -209,15 +198,15 @@ sub session_tick
$logger->log(LOG_DEBUG,"[TCSTATS] TASK/".$task->ID.": Starting '$cmdStr' as ".$task->ID." with PID ".$task->PID);
# Set last time we were run to now
$lastStats->{$iface} = $now;
$lastStats->{$interface} = $now;
# NK: Space the stats out, this will cause TICK_PERIOD to elapse before we do another interface
$ifaces++;
$interfaceCount++;
last;
}
# If we didn't fire up any stats, re-tick
if (!$ifaces) {
if (!$interfaceCount) {
$kernel->delay(tick => TICK_PERIOD);
}
};
......@@ -232,39 +221,45 @@ sub task_child_stdout
# Grab task data
my $taskData = $heap->{'task_data'}->{$task_id};
my $iface = $taskData->{'iface'};
my $direction = $taskData->{'direction'};
my $interface = $taskData->{'interface'};
my $timestamp = $taskData->{'timestamp'};
# Limit ID to update
my $lid;
# Stats ID to update
my $sid;
# Default to transmit statistics
my $direction = opentrafficshaper::plugins::statistics::STATISTICS_DIR_TX;
# Is this a system class?
# XXX: _class_parent is hard coded to 1
if ($stat->{'_class_parent'} == 1 && (my $classChildDec = hex($stat->{'_class_child'})) < 100) {
# Split off the different types of updates
if ($classChildDec == 1) {
$lid = "main:${iface}";
$sid = opentrafficshaper::plugins::statistics::getSIDFromCID($interface,0);
} else {
# Save the class with the decimal number
if (my $classID = opentrafficshaper::plugins::tc::isTcTrafficClassValid($stat->{'_class_child'})) {
$lid = "main:${iface}:$classID";
if (my $tcClass = opentrafficshaper::plugins::tc::isTcTrafficClassValid($interface,1,$stat->{'_class_child'})) {
my $classID = hex($tcClass);
$sid = opentrafficshaper::plugins::statistics::getSIDFromCID($interface,$classID);
} else {
$logger->log(LOG_WARN,"[TCSTATS] System traffic class '%s:%s' NOT FOUND",$stat->{'_class_parent'},$stat->{'_class_child'});
}
}
} else {
$lid = opentrafficshaper::plugins::tc::getLIDFromTcClass($stat->{'_class_child'});
if (defined(my $lid = opentrafficshaper::plugins::tc::getLIDFromTcLimitClass($interface,$stat->{'_class_child'}))) {
$sid = opentrafficshaper::plugins::statistics::getSIDFromLID($lid);
$direction = opentrafficshaper::plugins::statistics::getTrafficDirection($lid,$interface);
}
}
# Make sure we have the lid now
if (defined($lid)) {
if (defined($sid)) {
# Build our submission
$stat->{'timestamp'} = $timestamp;
$stat->{'direction'} = $direction;
$taskData->{'stats'}->{$lid} = $stat;
$taskData->{'stats'}->{$sid} = $stat;
}
}
......@@ -307,6 +302,7 @@ sub task_child_close
$kernel->delay(tick => TICK_PERIOD);
}
# Reap the dead child
sub task_sigchld
{
......
......@@ -40,7 +40,18 @@ use opentrafficshaper::logger;
use opentrafficshaper::plugins;
use opentrafficshaper::utils qw( parseURIQuery parseFormContent isUsername isIP isNumber prettyUndef );
use opentrafficshaper::plugins::configmanager qw( getLimits getLimit getTrafficClasses getTrafficClassName isTrafficClassValid );
use opentrafficshaper::plugins::configmanager qw(
getLimits getLimit
getInterfaceGroups
isInterfaceGroupValid
getMatchPriorities
isMatchPriorityValid
getTrafficClasses getTrafficClassName
isTrafficClassValid
);
......@@ -270,6 +281,8 @@ sub limit_addedit
my @formElements = qw(
FriendlyName
Username IP
InterfaceGroupID
MatchPriorityID
ClassID
TrafficLimitTx TrafficLimitTxBurst
TrafficLimitRx TrafficLimitRxBurst
......@@ -344,6 +357,14 @@ sub limit_addedit
if (!defined($ipAddress = isIP($formData->{'IP'}))) {
push(@errors,"IP address is not valid");
}
my $interfaceGroupID;
if (!defined($interfaceGroupID = isInterfaceGroupValid($formData->{'InterfaceGroupID'}))) {
push(@errors,"Interface group is not valid");
}
my $matchPriorityID;
if (!defined($matchPriorityID = isMatchPriorityValid($formData->{'MatchPriorityID'}))) {
push(@errors,"Match priority is not valid");
}
my $classID;
if (!defined($classID = isTrafficClassValid($formData->{'ClassID'}))) {
push(@errors,"Traffic class is not valid");
......@@ -397,6 +418,8 @@ sub limit_addedit
'Username' => $username,
'IP' => $ipAddress,
'GroupID' => 1,
'InterfaceGroupID' => $interfaceGroupID,
'MatchPriorityID' => $matchPriorityID,
'ClassID' => $classID,
'TrafficLimitTx' => $trafficLimitTx,
'TrafficLimitTxBurst' => $trafficLimitTxBurst,
......@@ -414,11 +437,13 @@ sub limit_addedit
$kernel->post("configmanager" => "process_limit_change" => $limit);
$logger->log(LOG_INFO,'[WEBSERVER/LIMITS] Acount: %s, User: %s, IP: %s, Group: %s, Class: %s, Limits: %s/%s, Burst: %s/%s',
$logger->log(LOG_INFO,'[WEBSERVER/LIMITS] Acount: %s, User: %s, IP: %s, Group: %s, InterfaceGroup: %s, MatchPriority: %s, Class: %s, Limits: %s/%s, Burst: %s/%s',
$formType,
prettyUndef($username),
prettyUndef($ipAddress),
prettyUndef(undef),
prettyUndef($interfaceGroupID),
prettyUndef($matchPriorityID),
prettyUndef($classID),
prettyUndef($trafficLimitTx),
prettyUndef($trafficLimitRx),
......@@ -451,6 +476,36 @@ EOF
}
}
# Generate interface group list
my $interfaceGroups = getInterfaceGroups();
my $interfaceGroupStr = "";
foreach my $interfaceGroupID (sort keys %{$interfaceGroups}) {
# Process selections nicely
my $selected = "";
if ($formData->{'InterfaceGroupID'} ne "" && $formData->{'InterfaceGroupID'} eq $interfaceGroupID) {
$selected = "selected";
}
# And build the options
$interfaceGroupStr .= '<option value="'.$interfaceGroupID.'" '.$selected.'>'.$interfaceGroups->{$interfaceGroupID}->{'name'}.'</option>';
}
# Generate match priority list
my $matchPriorities = getMatchPriorities();
my $matchPriorityStr = "";
foreach my $matchPriorityID (sort keys %{$matchPriorities}) {
# Process selections nicely
my $selected = "";
if ($formData->{'MatchPriorityID'} ne "" && $formData->{'MatchPriorityID'} eq $matchPriorityID) {
$selected = "selected";
}
# Default to 2 if nothing specified
if ($formData->{'MatchPriorityID'} eq "" && $matchPriorityID eq "2") {
$selected = "selected";
}
# And build the options
$matchPriorityStr .= '<option value="'.$matchPriorityID.'" '.$selected.'>'.$matchPriorities->{$matchPriorityID}.'</option>';
}
# Generate traffic class list
my $trafficClasses = getTrafficClasses();
my $trafficClassStr = "";
......@@ -499,6 +554,23 @@ EOF
</div>
</div>
</div>
<div class="form-group">
<label for="InterfaceGroupID" class="col-lg-2 control-label">Interface Group</label>
<div class="row">
<div class="col-lg-2">
<select name="InterfaceGroupID" class="form-control" $formNoEdit>
$interfaceGroupStr
</select>
</div>
<label for="MatchPriorityID" class="col-lg-2 control-label">Match Priority</label>
<div class="col-lg-2">
<select name="MatchPriorityID" class="form-control" $formNoEdit>
$matchPriorityStr
</select>
</div>
</div>
</div>
<div class="form-group">
<label for="ClassID" class="col-lg-2 control-label">Traffic Class</label>
<div class="row">
......
......@@ -36,18 +36,27 @@ use HTTP::Status qw( :constants );
use JSON;
use opentrafficshaper::logger;
use opentrafficshaper::plugins;
use opentrafficshaper::utils qw( parseURIQuery );
use opentrafficshaper::plugins::configmanager qw( getLimit );
use opentrafficshaper::plugins::configmanager qw( getLimit getInterface isTrafficClassValid getTrafficClassName );
use opentrafficshaper::plugins::statistics::statistics;
# Graphs by limit
sub bylimit
{
my ($kernel,$globals,$client_session_id,$request) = @_;
# If the plugin is not loaded, we cannot pull stats
if (!plugin_is_loaded('statistics')) {
return undef;
}
# Header
my $content = <<EOF;
<div id="header">
<h2>Limit Stats View</h2>
......@@ -56,17 +65,18 @@ EOF
my $limit;
# Maybe we were given an override key as a parameter? this would be an edit form
# Check request
if ($request->method eq "GET") {
# Parse GET data
my $queryParams = parseURIQuery($request);
# We need a key first of all...
# We need our LID
if (!defined($queryParams->{'lid'})) {
$content .=<<EOF;
<tr class="info">
<td colspan="8"><p class="text-center">No LID in Query String</p></td>
</tr>
EOF
goto END;
}
# Check if we get some data back when pulling the limit from the backend
if (!defined($limit = getLimit($queryParams->{'lid'}))) {
......@@ -97,8 +107,6 @@ EOF
</div>
EOF
# FIXME - Dynamic script inclusion required
#$content .= statistics::do_test();
# $content .= opentrafficshaper::plugins::statistics::do_test();
......@@ -117,6 +125,254 @@ EOF
# String put in <script> </script> tags after the above files are loaded
my $javascript = _getJavascript($dataPathStr);
END:
return (HTTP_OK,$content,{ 'javascripts' => \@javascripts, 'javascript' => $javascript });
}
# Return data by limit
sub databylimit
{
my ($kernel,$globals,$client_session_id,$request) = @_;
# If the plugin is not loaded, we cannot pull stats
if (!plugin_is_loaded('statistics')) {
return undef;
}
# Parse GET data
my $queryParams = parseURIQuery($request);
# Check if the limit ID was passed to us
if (!defined($queryParams->{'lid'})) {
return (HTTP_OK,{ 'error' => 'Invalid lid' },{ 'type' => 'json' });
}
my $limit;
if (!defined($limit = getLimit($queryParams->{'lid'}))) {
return (HTTP_OK,{ 'error' => 'Invalid limit' },{ 'type' => 'json' });
}
# Pull in stats data
my $statsData = opentrafficshaper::plugins::statistics::getStatsByLID($queryParams->{'lid'});
# First stage refinement
my $rawData;
foreach my $timestamp (sort keys %{$statsData}) {
foreach my $direction (keys %{$statsData->{$timestamp}}) {
foreach my $stat ('rate','pps','cir','limit') {
push( @{$rawData->{"$direction.$stat"}->{'data'}} , [ $timestamp , $statsData->{$timestamp}->{$direction}->{$stat} ] );
}
}
}
# Second stage - add labels
foreach my $direction ('tx','rx') {
foreach my $stat ('rate','pps','cir','limit') {
# Make it looks nice: Tx Rate
my $label = uc($direction) . " " . ucfirst($stat);
# And set it as the label
$rawData->{"$direction.$stat"}->{'label'} = $label;
}
}
# Final stage, chop it out how we need it
my $jsonData = [ $rawData->{'tx.limit'}, $rawData->{'rx.limit'}, $rawData->{'tx.rate'} , $rawData->{'rx.rate'} ];
return (HTTP_OK,$jsonData,{ 'type' => 'json' });
}
# Graphs by class
sub byclass
{
my ($kernel,$globals,$client_session_id,$request) = @_;
# If the plugin is not loaded, we cannot pull stats
if (!plugin_is_loaded('statistics')) {
return undef;
}
# Header
my $content = <<EOF;
<div id="header">
<h2>Class Stats View</h2>
</div>
EOF
my $iface;
my $cid;
# Check if its a GET request...
if ($request->method eq "GET") {
# Parse GET data
my $queryParams = parseURIQuery($request);
# Grab the interface name
if (!defined($queryParams->{'interface'})) {
$content .=<<EOF;
<tr class="info">
<td colspan="8"><p class="text-center">No interface in Query String</p></td>
</tr>
EOF
goto END;
}
# Grab the class
if (!defined($queryParams->{'class'})) {
$content .=<<EOF;
<tr class="info">
<td colspan="8"><p class="text-center">No class in Query String</p></td>
</tr>
EOF
goto END;
}
# Check if we get some data back when pulling the iface from the backend
if (!defined($iface = getInterface($queryParams->{'interface'}))) {
$content .=<<EOF;
<tr class="info">
<td colspan="8"><p class="text-center">No Interface Results</p></td>
</tr>
EOF
goto END;
}
# Check if our traffic class is valid
if (!defined($cid = isTrafficClassValid($queryParams->{'class'})) && $queryParams->{'class'} ne "0") {
$content .=<<EOF;
<tr class="info">
<td colspan="8"><p class="text-center">No Class Results</p></td>
</tr>
EOF
goto END;
}
}
my $interfaceEncoded = encode_entities($iface->{'Interface'});
my $interfaceNameEncoded = encode_entities($iface->{'Name'});
my $classNameEncoded;
if ($cid) {
$classNameEncoded = encode_entities(getTrafficClassName($cid));
} else {
$classNameEncoded = $interfaceNameEncoded;
}
# Build content
$content = <<EOF;
<div id="content" style="float:left">
<div style="position: relative; top: 50px;">
<h4 style="color:#8f8f8f;">Latest Data For: $classNameEncoded on $interfaceEncoded</h4>
<br/>
<div id="ajaxData" class="ajaxData" style="float:left; width:1024px; height: 560px"></div>
</div>
</div>
EOF
# Files loaded at end of HTML document
my @javascripts = (
'/static/awit-flot/jquery.flot.min.js',
'/static/awit-flot/jquery.flot.time.min.js',
);
# Build our data path using the URI module to make sure its nice and clean
my $dataPath = URI->new('/statistics/data-by-class');
# Pass it the original query, just incase we had extra params we can use
$dataPath->query_form( $request->uri->query_form() );
my $dataPathStr = $dataPath->as_string();
# String put in <script> </script> tags after the above files are loaded