Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • opentrafficshaper/opentrafficshaper
  • eezysol/opentrafficshaper
  • sudesh/opentrafficshaper
  • Yuriy/opentrafficshaper
  • nkukard/opentrafficshaper
  • fcardoso/opentrafficshaper
6 results
Show changes
Showing
with 9736 additions and 236 deletions
# Basic radius dictionary
# Copyright (C) 2009-2013, AllWorldIT
# Copyright (C) 2009-2015, 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
......
# AllWorldIT vendor radius dictionary
# Copyright (C) 2009-2013, AllWorldIT
# Copyright (C) 2009-2015, 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
......@@ -16,9 +16,10 @@
VENDOR AllWorldIT 11111
VENDOR AllWorldIT 42109
ATTRIBUTE OpenTrafficShaper-Traffic-Limit 1 string AllWorldIT
ATTRIBUTE OpenTrafficShaper-Traffic-Group 2 integer AllWorldIT
ATTRIBUTE OpenTrafficShaper-Traffic-Class 3 integer AllWorldIT
ATTRIBUTE OpenTrafficShaper-Traffic-Pool 4 string AllWorldIT
[system]
# Log level
# 0 - Errors only
# 1 - Warnings and errors
# 2 - Notices, warnings, errors
# 3 - Info, notices, warnings, errors
# 4 - Debugging
#
# default:
# log_level=2
# Log file to write log messages to
#
# default:
# log_file=/var/log/opentrafficshaper/opentrafficshaper.log
# PID file to write our PID to
#
# default:
# pid_file=/run/opentrafficshaper/opentrafficshaper.pid
# State file, this file is used to store persistent information
#
# default:
# statefile=/var/lib/opentrafficshaper/configmanager.state
#
# Plugins
#
[plugins]
# Names of plugins to load, one per line
# NOTE: The ordering is very important
# Radius support
load=radius
# Statistics
# Must load before webserver if you want graphs
load=statistics
# Web interface
load=webserver
# Statistics live streaming snapin
# Must load after webserver
load=webserver/snapins/websockets/statistics
# Traffic shaper
load=tc
# Traffic shaper interface to statistics
# Must load after statistics
# Must load after tc
load=tcstats
#
# General shaping settings
#
[shaping]
# User group, this is the list of groups users can belong to
#
# The format of this option is:
# <ID>:<DESCRIPTION>
#
# * NOT IMPLEMENTED YET *
#
# default:
# none
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 format of this option is:
# <ID>:<DESCRIPTION>
#
# default:
# none
class=1:High Priority
class=2:Platinum
class=3:Gold
class=4:Silver
class=5:Bronze
class=6:Best Effort
# Default pool for traffic not classified, we can send it to a specific traffic class. This is a pool ID.
#
# default:
# default_pool=no
# Interface groups that a pool is associated with
#
# The format of this option is:
# <TXIFACE>,<RXIFACE>:<DESCRIPTION>
#
# 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)
#
# default:
# interface_group=eth1,eth0:Default
interface_group=eth1,eth0:LAN-side
#
# Interface setup
#
# Each interface comprises of a name, rate and a list of class rates for each class defined above. Each interface used in the
# interface_groups above must be defined below.
#
[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 rate specification
#
# format:
# <CLASSID>:<CIR>[/<LIMIT>]
#
# The CIR and Limit are specified in Kbps or percentage
# If Limit is not specified it defaults to CIR
# if the entire class definition is omitted, defaults to rate of interface
#
# default:
# --interface limit for each class--
class_rate=1:10000
class_rate=2:5%/5%
class_rate=3:5%
class_rate=4:5000/10000
class_rate=5:5%
class_rate=6:5%
[shaping.interface eth1]
name=LAN Interface
rate=100000
class_rate=1:70000
class_rate=2:5%/5%
class_rate=3:5%
class_rate=4:3000/5000
class_rate=5:5%
#
# Radius plugin
#
[plugin.radius]
# Path of the radius dictionary files
#
# default:
# none
dictionary_path=/etc/opentrafficshaper
# Dictionaries we need to load for radius attributes we use, these are paths relative to dicitonary_path=
#
# default:
# none
dictionary=dicts/dictionary
dictionary=dicts/dictionary.allworldit
# Expire traffic control entries from radius in this period of time if not updated, this is in seconds
#
# default:
# expiry_period=86400
# Pool name transform to apply to the username. We apply a regex to the username and grab the first returned group, this group is
# then used as the pool name instead of the full username.
#
#
# Example: To use user@POOL, try something like this...
# username_to_pool_transform=^[^@]+@(.*)
#
# Example: To use user.POOL@realm, try something like this...
# username_to_pool_transform=^[^\.]+\.([^@]+)
#
# default:
# none
# Interface group to use for users which don't have the attribute set
#
# default:
# default_interface_group=1
# Match priority to use for users which don't have the attribute set
#
# default:
# default_match_priority=2
# Traffic class to use for users which don't have the attribute set
#
# default:
# default_traffic_class=2
# NOT IMPLEMENTED: Default group to use for users which don't have the attribute set
#
# default:
# default_group=1
#
# TC Plugin
#
[plugin.tc]
# Protocol to filter on, 99% of the time it will be "ip". If however you're filtering VLAN Q-in-Q traffic, set this to 0x88a8
#
# default:
# protocol=ip
# IP Header offset, if the kernel offsets your IP packet with octets you need to specify the value here this most commonly happens
# when you shaping vlan traffic (as per above one would maybe set this value to 4)
#
# default:
# iphdr_offset=0
#
# Statistics Plugin
#
[plugin.statistics]
# Database credentials used for stats recording
#
# example:
# db_dsn=DBI:mysql:dbname=ots
# db_username=
# db_password=
[dictionary]
load=dicts/dictionary
load=dicts/dictionary.allworldit
# POE::Filter::HybridHTTP - Copyright 2007-2023, AllworldIT
# Hybrid HTTP filter supporting websockets too.
#
# 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/>.
##
# Code originally based on POE::Filter::HTTPD
##
# Filter::HTTPD Copyright 1998 Artur Bergman <artur@vogon.se>.
# Thanks go to Gisle Aas for his excellent HTTP::Daemon. Some of the
# get code was copied out if, unfortunately HTTP::Daemon is not easily
# subclassed for POE because of the blocking nature.
# 2001-07-27 RCC: This filter will not support the newer get_one()
# interface. It gets single things by default, and it does not
# support filter switching. If someone absolutely needs to switch to
# and from HTTPD filters, they should submit their request as a patch.
##
package opentrafficshaper::POE::Filter::HybridHTTP;
use warnings;
use strict;
use bytes;
use POE::Filter;
use opentrafficshaper::POE::Filter::HybridHTTP::WebSocketFrame;
use vars qw($VERSION @ISA);
# NOTE - Should be #.### (three decimal places)
$VERSION = '2.000';
@ISA = qw(POE::Filter);
# States of the protocol
use constant {
CRLF => "\r\n",
# Protocol states
ST_HTTP_HEADERS => 1, # Busy processing headers
ST_HTTP_CONTENT => 2, # Busy processing the body
ST_WEBSOCKET_STREAM => 3,
};
use Digest::SHA qw( sha1_base64 );
use HTTP::Date qw(time2str);
use HTTP::Request;
use HTTP::Response;
use HTTP::Status qw( :constants :is );
use URI;
my $HTTP_1_0 = _http_version("HTTP/1.0");
my $HTTP_1_1 = _http_version("HTTP/1.1");
# Class instantiation
sub new
{
my $class = shift;
# These are our internal properties
my $self = { };
# Build our class
bless($self, $class);
# And initialize
$self->_reset();
return $self;
}
# From the docs:
# get_one_start() accepts an array reference containing unprocessed stream chunks. The chunks are added to the filter's Internal
# buffer for parsing by get_one().
sub get_one_start
{
my ($self, $stream) = @_;
# Join all the blocks of data and add to our buffer
$self->{'buffer'} .= join('',@{$stream});
return $self;
}
# This is called to see if we can grab records/items
sub get_one
{
my $self = shift;
# Waiting for a complete suite of headers.
if ($self->{'state'} == ST_HTTP_HEADERS) {
return $self->_get_one_http_headers();
# Waiting for content.
} elsif ($self->{'state'} == ST_HTTP_CONTENT) {
return $self->_get_one_http_content();
# Websocket
} elsif ($self->{'state'} == ST_WEBSOCKET_STREAM) {
return $self->_get_one_websocket_record();
# XXX - better handling?
} else {
die "Unknown state '".unpack("H*",$self->{'state'})."'";
}
}
# Function to push data to the socket
sub put
{
my ($self, $responses) = @_;
my @results;
# Handle HTTP content
if ($self->{'state'} == ST_HTTP_CONTENT || $self->{'state'} == ST_HTTP_HEADERS) {
# Compile our list of results
foreach my $response (@{$responses}) {
# Check if we have a upgrade header
if ((my $h_upgrade = $response->header("Upgrade")) && $response->code == HTTP_SWITCHING_PROTOCOLS) {
# Check if its a websocket upgrade
if (
# Is it a request and do we have a original request?
$h_upgrade eq "websocket" && defined($self->{'last_request'}) &&
# If so was there a websocket-key?
(my $websocketKey = $self->{'last_request'}->header('Sec-WebSocket-Key'))
) {
$self->{'state'} = ST_WEBSOCKET_STREAM;
# GUID for this protocol as per RFC6455 Section 1.3
my $websocketKeyResponseRaw = $websocketKey."258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
my $websocketKeyResponse = sha1_base64($websocketKeyResponseRaw);
# Pad up to base64 length 4[N/3]
$websocketKeyResponse .= "=" x ((length($websocketKeyResponse) * 3) % 4);
$response->push_header('Sec-WebSocket-Accept',$websocketKeyResponse);
}
}
push(@results,$self->_build_raw_response($response));
}
# Handle WebSocket data
} elsif ($self->{'state'} == ST_WEBSOCKET_STREAM) {
# Compile our list of results
foreach my $response (@{$responses}) {
# If we don't have a websocket state, create one
if (!$self->{'websocket_state'}) {
$self->{'websocket_state'} = new opentrafficshaper::POE::Filter::HybridHTTP::WebSocketFrame();
}
# Don't mask replies from server to client RFC6455 secion 5.1.
$self->{'websocket_state'}->masked(0);
# Consume the response with websockets...
$self->{'websocket_state'}->append($response);
# Spit out bytes...
my $payload = $self->{'websocket_state'}->to_bytes();
push(@results,$payload);
}
}
return \@results;
}
#
# Internal functions
#
# Prepare for next request
sub _reset
{
my $self = shift;
# Reset our filter state
$self->{'buffer'} = '';
$self->{'state'} = ST_HTTP_HEADERS;
$self->{'websocket_state'} = undef;
$self->{'last_request'} = $self->{'request'};
$self->{'request'} = undef; # We want the last request always
$self->{'content_len'} = 0;
$self->{'content_added'} = 0;
}
# Internal function to parse an HTTP status line and return the HTTP
# protocol version.
sub _http_version
{
my $version = shift;
# Rip off the version string
if ($version =~ m,^(?:HTTP/)?(\d+)\.(\d+)$,i) {
my $nversion = $1 * 1000 + $2;
# Return a numerical version of it
return $nversion;
} else {
# Or 0 if we did not match
return 0;
}
}
# Function to handle HTTP headers
sub _get_one_http_headers
{
my $self = shift;
# Strip leading whitespace.
$self->{'buffer'} =~ s/^\s+//;
# If we've not found the HTTP headers, just return a blank arrayref
if ($self->{'buffer'} !~ s/^(\S.*?(?:\r?\n){2})//s) {
return [ ];
}
# Pull the headers as a string off the buffer
my $header_str = $1;
# Parse the request line.
if ($header_str !~ s/^(\w+)[ \t]+(\S+)(?:[ \t]+(HTTP\/\d+\.\d+))?[^\n]*\n//) {
return [
$self->_build_error(HTTP_BAD_REQUEST, "Request line parse failure. ($header_str)")
];
}
# Create an HTTP::Request object from values in the request line.
my ($method, $uri, $proto) = ($1, $2, $3);
# Make sure proto is set
if (!_http_version($proto)) {
$proto = "HTTP/1.0";
}
$self->{'protocol'} = $proto;
$proto = _http_version($proto);
# Fix a double starting slash on the path. It happens.
$uri =~ s!^//+!/!;
# Build our request object
my $request = HTTP::Request->new($method, URI->new($uri));
# Set protocol
$request->protocol($self->{'protocol'});
# Parse headers.
my ($key, $val);
HEADER: while ($header_str =~ s/^([^\012]*)\012//) {
my $header = $1;
$header =~ s/\015$//;
if ($header =~ /^([\w\-~]+)\s*:\s*(.*)/) {
# If we had a key, it means we must save this key/value pair
if ($key) {
$request->push_header($key, $val);
}
# Assign key and value pair from above regex
($key, $val) = ($1, $2);
# Multi-line header value
} elsif ($header =~ /^\s+(.*)/) {
$val .= " $1";
# We no longer matching, so this is the last header?
} else {
last HEADER;
}
}
# Push on the last header if we had one...
$request->push_header($key, $val) if $key;
# We got a full set of headers. Fall through to content if we
# have a content length.
my $content_length = $request->content_length();
if(defined($content_length)) {
$content_length =~ s/\D//g;
# If its invalid, it will be 0 anyway
$content_length = int($content_length);
}
my $content_encoding = $request->content_encoding();
# The presence of a message-body in a request is signaled by the
# inclusion of a Content-Length or Transfer-Encoding header field in
# the request's message-headers. A message-body MUST NOT be included in
# a request if the specification of the request method (section 5.1.1)
# does not allow sending an entity-body in requests. A server SHOULD
# read and forward a message-body on any request; if the request method
# does not include defined semantics for an entity-body, then the
# message-body SHOULD be ignored when handling the request.
# - RFC2616
if (!defined($content_length) && !defined($content_encoding)) {
$self->{'request'} = $request;
$self->_reset();
return [ $request ];
}
# PG- GET shouldn't have a body. But RFC2616 talks about Content-Length
# for HEAD. And My reading of RFC2616 is that HEAD is the same as GET.
# So logically, GET can have a body. And RFC2616 says we SHOULD ignore
# it.
#
# What's more, in apache 1.3.28, a body on a GET or HEAD is read
# and discarded. See ap_discard_request_body() in http_protocol.c and
# default_handler() in http_core.c
#
# Neither Firefox 2.0 nor Lynx 2.8.5 set Content-Length on a GET
# For compatibility with HTTP/1.0 applications, HTTP/1.1 requests
# containing a message-body MUST include a valid Content-Length header
# field unless the server is known to be HTTP/1.1 compliant. If a
# request contains a message-body and a Content-Length is not given,
# the server SHOULD respond with 400 (bad request) if it cannot
# determine the length of the message, or with 411 (length required) if
# it wishes to insist on receiving a valid Content-Length.
# - RFC2616
# PG- This seems to imply that we can either detect the length (but how
# would one do that?) or require a Content-Length header. We do the
# latter.
#
# PG- Dispite all the above, I'm not fully sure this implements RFC2616
# properly. There's something about transfer-coding that I don't fully
# understand.
if (!$content_length) {
# assume a Content-Length of 0 is valid pre 1.1
if ($proto >= $HTTP_1_1 && !defined($content_length)) {
# We have Content-Encoding, but not Content-Length.
$request = $self->_build_error(HTTP_LENGTH_REQUIRED,"No content length found.",$request);
}
$self->_reset();
return [ $request ];
}
$self->{'content_length'} = $content_length;
$self->{'state'} = ST_HTTP_CONTENT;
$self->{'request'} = $request;
$self->_get_one_http_content();
}
sub _get_one_http_content
{
my $self = shift;
my $request = $self->{'request'};
my $content_needed = $self->{'content_length'} - $self->{'content_added'};
if ($content_needed < 1) {
# We somehow got too much content
$request = $self->_build_error(HTTP_BAD_REQUEST, "Too much content received");
$self->_reset();
return [ $request ];
}
# Not enough content to complete the request. Add it to the
# request content, and return an incomplete status.
if ((my $buflen = length($self->{'buffer'})) < $content_needed) {
$request->add_content($self->{'buffer'});
$self->{'content_added'} += $buflen;
$self->{'buffer'} = '';
return [ ];
}
# Enough data. Add it to the request content.
# PG- CGI.pm only reads Content-Length: bytes from STDIN.
# Four-argument substr() would be ideal here, but it's not
# entirely backward compatible.
$request->add_content(substr($self->{'buffer'}, 0, $content_needed));
substr($self->{'buffer'}, 0, $content_needed) = "";
# Some browsers (like MSIE 5.01) send extra CRLFs after the
# content. Shame on them.
$self->{'buffer'} =~ s/^\s+//;
# XXX Should we throw the body away on a GET or HEAD? Probably not.
# XXX Should we parse Multipart Types bodies?
# Prepare for the next request, and return this one.
$self->_reset();
return [ $request ];
}
# Function to get a websocket record set
sub _get_one_websocket_record
{
my $self = shift;
# If we don't have a websocket state, create one
if (!$self->{'websocket_state'}) {
$self->{'websocket_state'} = new opentrafficshaper::POE::Filter::HybridHTTP::WebSocketFrame();
}
$self->{'websocket_state'}->append($self->{'buffer'});
# Blank our buffer
$self->{'buffer'} = '';
# Loop with records and push onto result set
my @results;
while (my $item = $self->{'websocket_state'}->next()) {
push(@results,$item);
}
return \@results;
}
# Build a basic error to return
sub _build_error
{
my($self, $status, $details, $request) = @_;
# Setup defaults
if (!defined($status)) {
$status = HTTP_BAD_REQUEST;
}
if (!defined($details)) {
$details = '';
}
my $message = "Unknown Error";
if (my $msg = status_message($status)) {
$message = $msg;
}
# Build the response object
my $response = new HTTP::Response->new($status,$message);
$response->content(<<EOF);
<!DOCTYPE html>
<html>
<head>
<title>$status $message</title>
</head>
<body>
<h1>$message</h1>
<p>$details</p>
</body>
</html>
EOF
# If we have a request set it
if ($request) {
$response->request($request);
}
return $response;
}
# Build a socket friendly response
sub _build_raw_response
{
my ($self,$response) = @_;
# Check for headers we should return
if (!defined($response->header("Date"))) {
$response->push_header("Date",time2str(time));
}
if (!defined($response->header("Server"))) {
$response->push_header("Server","POE Hybrid HTTP Server v$VERSION");
}
# Set our content length
# - This is required even if the content length is 0, for instance when we doing a REDIRECT with no content some browsers hang if
# - there is no content length set.
$response->header("Content-Length" => length($response->content));
# Setup our output
my $output = sprintf("%s %s",$self->{'protocol'},$response->status_line);
$output .= CRLF;
$output .= $response->headers_as_string(CRLF);
$output .= CRLF;
$output .= $response->content;
return $output;
}
1;
# vim: ts=4
# POE::Filter::HybridHTTP::WebSocketFrame - Copyright 2007-2023, AllworldIT
# Hybrid HTTP filter support for WebSocketFrames
#
# 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/>.
##
# Code originally based on Protocol::WebSocket::Frame
##
# CREDITS:
# Paul "LeoNerd" Evans
# Jon Gentle
# Lee Aylward
# Chia-liang Kao
# Atomer Ju
# Chuck Bredestege
# Matthew Lien (BlueT)
# AUTHOR:
# Viacheslav Tykhanovskyi, C<vti@cpan.org>.
# COPYRIGHT:
# Copyright (C) 2010-2012, Viacheslav Tykhanovskyi.
# This program is free software, you can redistribute it and/or modify it under
# the same terms as Perl 5.10.
##
package opentrafficshaper::POE::Filter::HybridHTTP::WebSocketFrame;
use bytes;
use strict;
use warnings;
use Config;
use Encode ();
# Random number generation
use constant MAX_RAND_INT => 2 ** 32;
use constant MATH_RANDOM_SECURE => eval "require Math::Random::Secure;";
our %TYPES = (
text => 0x01,
binary => 0x02,
ping => 0x09,
pong => 0x0a,
close => 0x08
);
use constant {
WS_MAX_FRAGMENTS => 1024,
WS_MAX_PAYLOAD_SIZE => 131072,
};
sub new {
my ($class,$buffer) = @_;
if (my $classRef = ref $class) {
$class = $classRef;
}
# If we don't have a buffer just use an empty string
if (!defined($buffer)) {
$buffer = "";
}
# Setup ourself
my $self = {
'buffer' => Encode::is_utf8($buffer) ? Encode::encode('UTF-8', $buffer) : $buffer,
'fragments' => [ ],
'max_payload_size' => WS_MAX_PAYLOAD_SIZE,
'max_fragments' => WS_MAX_FRAGMENTS,
};
bless($self,$class);
return $self;
}
sub append {
my ($self,$data) = @_;
# If there is no data just return
if (!defined($data)) {
return;
}
$self->{'buffer'} .= $data;
return $self;
}
sub next {
my $self = shift;
# If we have next_bytes return
if (defined(my $bytes = $self->next_bytes)) {
return Encode::decode('UTF-8', $bytes);
}
return;
}
sub fin { @_ > 1 ? $_[0]->{fin} = $_[1] : $_[0]->{fin} }
sub rsv { @_ > 1 ? $_[0]->{rsv} = $_[1] : $_[0]->{rsv} }
sub opcode { @_ > 1 ? $_[0]->{opcode} = $_[1] : $_[0]->{opcode} || 1 }
sub masked { @_ > 1 ? $_[0]->{masked} = $_[1] : $_[0]->{masked} }
sub is_ping { $_[0]->opcode == 9 }
sub is_pong { $_[0]->opcode == 10 }
sub is_close { $_[0]->opcode == 8 }
sub is_text { $_[0]->opcode == 1 }
sub is_binary { $_[0]->opcode == 2 }
sub next_bytes {
my $self = shift;
return unless length $self->{'buffer'} >= 2;
while (my $buffer_len = length($self->{'buffer'})) {
my $offset = 0;
# Grab first byte
my $hdr = substr($self->{'buffer'}, $offset, 1);
# Reduce first hdr byte to bits
my @bits = split //, unpack("B*", $hdr);
# Set the FIN attribute
$self->fin($bits[0]);
# And the RSV (reserved)
$self->rsv([@bits[1 .. 3]]);
# Pull off the opcode & update offset
my $opcode = unpack('C', $hdr) & 0b00001111;
$offset += 1; # FIN,RSV[1-3],OPCODE
# Grab payload length
my $payload_len = unpack('C',substr($self->{'buffer'}, $offset, 1));
# Check if the payload is masked, if it is flag it internally
my $masked = ($payload_len & 0b10000000) >> 7;
$self->masked($masked);
$offset += 1; # + MASKED,PAYLOAD_LEN
$payload_len = $payload_len & 0b01111111;
if ($payload_len == 126) {
# Not enough data
if ($buffer_len < $offset + 2) {
return;
}
# Unpack the payload_len into its actual value & bump the offset
$payload_len = unpack('n',substr($self->{'buffer'},$offset,2));
$offset += 2;
} elsif ($payload_len > 126) {
# Not enough data
if ($buffer_len < $offset + 4) {
return;
}
# Map off the first 8 bits
my $bits = unpack('B*',substr($self->{'buffer'},$offset,8));
# Most significant bit must be 0
substr($bits,0,1,0);
# Can we not handle 64bit numbers?
if ($Config{'ivsize'} <= 4 || $Config{longsize} < 8) {
# If not, just use 32 bits
$bits = substr($bits, 32);
$payload_len = unpack('N',pack('B*',$bits));
# If we can use everything we have
} else {
$payload_len = unpack('Q>',pack('B*',$bits));
}
# Bump offset
$offset += 8;
}
# XXX - not sure how to return this sanely
if ($payload_len > $self->{'max_payload_size'}) {
$self->{'buffer'} = '';
return;
}
# Grab the mask
my $mask;
if ($self->masked) {
# Not enough data
if ($buffer_len < $offset + 4) {
return;
}
# Pull it off
$mask = substr($self->{'buffer'}, $offset, 4);
$offset += 4;
}
# Check if we have enough data to satisfy our payload_len
if ($buffer_len < $offset + $payload_len) {
return;
}
# If we do, rip it all off and shove it into $payload
my $payload = substr($self->{'buffer'}, $offset, $payload_len);
# If our data is masked, unmask it
if ($self->masked) {
$payload = $self->_mask($payload, $mask);
}
substr($self->{'buffer'}, 0, $offset + $payload_len, '');
# Injected control frame
if (@{$self->{'fragments'}} && $opcode & 0b1000) {
$self->opcode($opcode);
return $payload;
}
# Check if this is the last packet in a set of fragments, if it is combine everything
if ($self->fin) {
if (@{$self->{'fragments'}}) {
$self->opcode(shift @{$self->{'fragments'}});
} else {
$self->opcode($opcode);
}
# Join everything up
$payload = join('',@{$self->{'fragments'}},$payload);
$self->{'fragments'} = [];
# And return
return $payload;
} else {
# Remember first fragment opcode
if (!@{$self->{'fragments'}}) {
push @{$self->{'fragments'}}, $opcode;
}
push(@{$self->{'fragments'}},$payload);
# XXX - Handle sanely?
if (@{$self->{'fragments'}} > $self->{'max_fragments'}) {
$self->{'fragments'} = [];
$self->{'buffer'} = '';
return;
}
}
}
return;
}
sub to_bytes {
my $self = shift;
my $string = '';
my $opcode;
if (my $type = $self->{'type'}) {
$opcode = $TYPES{$type};
}
else {
$opcode = $self->opcode || 1;
}
# Set FIN + black RSV + set OPCODE in the first 8 bites
$string .= pack('C',($opcode | 0b10000000) & 0b10001111);
my $payload_len = length($self->{'buffer'});
if ($payload_len <= 125) {
# Flip masked bit if we're masked
$payload_len |= 0b10000000 if $self->masked;
# Encode the payload length and add to string
$string .= pack('C',$payload_len);
}
elsif ($payload_len <= 0xffff) {
my $bits = 0b01111110;
$bits |= 0b10000000 if $self->masked;
$string .= pack('C',$bits);
$string .= pack('n',$payload_len);
}
else {
my $bits = 0b01111111;
$bits |= 0b10000000 if $self->masked;
$string .= pack('C',$bits);
# Shifting by an amount >= to the system wordsize is undefined
$string .= pack('N',$Config{'ivsize'} <= 4 ? 0 : $payload_len >> 32);
$string .= pack('N',($payload_len & 0xffffffff));
}
if ($self->masked) {
my $mask = $self->{mask} || ( MATH_RANDOM_SECURE ? Math::Random::Secure::irand(MAX_RAND_INT) : int(rand(MAX_RAND_INT)) );
$mask = pack('N',$mask);
$string .= $mask;
$string .= $self->_mask($self->{'buffer'},$mask);
}
else {
$string .= $self->{'buffer'};
}
# Wipe buffer
$self->{'buffer'} = '';
return $string;
}
sub _mask {
my $self = shift;
my ($payload, $mask) = @_;
$mask = $mask x (int(length($payload) / 4) + 1);
$mask = substr($mask, 0, length($payload));
$payload ^= $mask;
return $payload;
}
1;
# vim: ts=4
# OpenTrafficShaper POE::Filter::TCStatistics TC stats filter
# OpenTrafficShaper webserver module: limits page
# Copyright (C) 2007-2023, 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/>.
##
# Code originally based on POE::Filter::HTTPD
##
# Filter::HTTPD Copyright 1998 Artur Bergman <artur@vogon.se>.
# Thanks go to Gisle Aas for his excellent HTTP::Daemon. Some of the
# get code was copied out if, unfortunately HTTP::Daemon is not easily
# subclassed for POE because of the blocking nature.
# 2001-07-27 RCC: This filter will not support the newer get_one()
# interface. It gets single things by default, and it does not
# support filter switching. If someone absolutely needs to switch to
# and from HTTPD filters, they should submit their request as a patch.
##
package opentrafficshaper::POE::Filter::TCStatistics;
use warnings;
use strict;
use JSON qw(decode_json);
use POE::Filter;
use vars qw($VERSION @ISA);
# NOTE - Should be #.### (three decimal places)
$VERSION = '3.000';
@ISA = qw(POE::Filter);
# Class instantiation
sub new
{
my $class = shift;
# These are our internal properties
my $self = { };
# Build our class
bless($self, $class);
# And initialize
$self->_reset();
return $self;
}
# From the docs:
# get_one_start() accepts an array reference containing unprocessed stream chunks. The chunks are added to the filter's Internal
# buffer for parsing by get_one().
sub get_one_start
{
my ($self, $stream) = @_;
# Join all the blocks of data and add to our buffer
$self->{'buffer'} .= join('',@{$stream});
return $self;
}
# This is called to see if we can grab records/items
sub get_one
{
my $self = shift;
my @results = ();
# If we have a empty buffer we can just return
return [] if ($self->{'buffer'} eq '');
chomp($self->{'buffer'});
# NK: It seems we may not get the terminating ] at the end if the output is still busy?
return [] if (substr($self->{'buffer'},-1) ne "]");
# Try decode JSON payload
my $items;
eval {
$items = decode_json($self->{'buffer'});
$self->{'buffer'} = '';
1;
} or do {
print(STDERR "FAILED TO DECODE JSON: >" . $self->{'buffer'} . "<\n");
return [];
};
# Loop with each item and generate a current stat
for my $item (@{$items}) {
my $curstat = {};
# Skip everything except HTB
if ($item->{'class'} ne "htb") {
continue;
}
# Split off the handle into the parent and child
( $curstat->{'TCClassParent'}, $curstat->{'TCClassChild'} ) = split( /:/, $item->{'handle'} );
# The rate and ceil is represented in bytes/s, so it needs to be multiplied by 8 and divided by 1000 to get Kbit/s
$curstat->{'CIR'} = (int($item->{'rate'}) * 8) / 1000;
$curstat->{'Limit'} = (int($item->{'ceil'}) * 8) / 1000;
# If we have a prio, we need to add this too
if (defined($item->{'prio'})) {
$curstat->{'Priority'} = int($item->{'prio'});
}
# We should probably always have these
$curstat->{'TotalBytes'} = int($item->{'stats'}->{'bytes'}) // 0;
$curstat->{'TotalPackets'} = int($item->{'stats'}->{'packets'}) // 0;
$curstat->{'TotalDropped'} = int($item->{'stats'}->{'drops'}) // 0;
$curstat->{'TotalOverLimits'} = int($item->{'stats'}->{'overlimits'}) // 0;
$curstat->{'QueueSize'} = int( $item->{'stats'}->{'backlog'} ) // 0;
$curstat->{'QueueLen'} = int( $item->{'stats'}->{'qlen'} ) // 0;
# These are HTB specific if stats are enabled
$curstat->{'Rate'} = 0;
if (defined($item->{'stats'}->{'rate'})) {
$curstat->{'Rate'} = (int($item->{'stats'}->{'rate'}) * 8) / 1000;
}
$curstat->{'PPS'} = 0;
if (defined($item->{'stats'}->{'pps'})) {
$curstat->{'PPS'} = int($item->{'stats'}->{'pps'});
}
push(@results,$curstat);
}
return [ @results ];
}
# Function to push data to the socket
sub put
{
my ($self, $data) = @_;
my @results = [ $data ];
return \@results;
}
#
# Internal functions
#
# Prepare for next request
sub _reset
{
my $self = shift;
# Reset our filter state
$self->{'buffer'} = '';
}
# Get rate...
sub _getKNumber
{
my $str = shift;
my ($num,$multiplier) = ($str =~ /([0-9]+)([KMG])?/);
# We only work in Kbit
if (!defined($multiplier)) {
$num /= 1000;
} elsif ($multiplier eq "K") {
# noop
} elsif ($multiplier eq "M") {
$num *= 1000;
} elsif ($multiplier eq "G") {
$num *= 1000000;
}
return int($num);
}
1;
# OpenTrafficShaper constants package
# Copyright (C) 2013, AllWorldIT
#
# Copyright (C) 2007-2023, 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
......@@ -25,16 +25,39 @@ require Exporter;
our (@ISA,@EXPORT,@EXPORT_OK);
@ISA = qw(Exporter);
@EXPORT = qw(
CFGM_NEW
CFGM_OFFLINE
CFGM_ONLINE
CFGM_CHANGED
SHAPER_NOTLIVE
SHAPER_PENDING
SHAPER_LIVE
SHAPER_CONFLICT
);
# CFGM_NEW - New
# CFGM_OFFLINE - Offline
# CFGM_ONLINE - Online
# CFGM_CHANGED - Changed
use constant {
CFGM_OFFLINE => 1,
CFGM_CHANGED => 2,
CFGM_ONLINE => 3,
CFGM_NEW => 4,
};
# SHAPER_NOTLIVE - Nothing is going on yet, something should happen
# SHAPER_PENDING - Waiting on shaper to do a change
# SHAPER_LIVE - Shaper is up to date with our config
# SHAPER_CONFLICT - Item is in conflict
use constant {
SHAPER_NOTLIVE => 0,
SHAPER_PENDING => 1,
SHAPER_LIVE => 2,
SHAPER_NOTLIVE => 1,
SHAPER_PENDING => 2,
SHAPER_LIVE => 4,
SHAPER_CONFLICT => 8,
};
......
# Logging functionality
# Copyright (C) 2007-2013, AllWorldIT
#
# Copyright (C) 2007-2023, 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
......@@ -45,6 +45,7 @@ use constant {
};
use IO::Handle;
use POSIX qw( strftime );
......@@ -52,11 +53,16 @@ use POSIX qw( strftime );
# Instantiate
sub new {
my ($class) = @_;
my $self = { };
my $self = {
'handle' => \*STDERR,
'level' => 2,
};
bless $self, $class;
return $self;
}
# Logging function
sub log
{
......@@ -78,18 +84,53 @@ sub log
# Parse message nicely
if ($msg =~ /^(\[[^\]]+\]) (.*)/s) {
$msg = "$1 $logtxt: $2";
$msg = sprintf("%s %s: %s",$1,$logtxt,$2);
} else {
$msg = "[UNKNOWN] $logtxt: $msg";
$msg = sprintf("[UNKNOWN] %s: %s",$logtxt,$msg);
}
# If we have args, this is more than likely a format string & args
if (@args > 0) {
$msg = sprintf($msg,@args);
}
# $self->SUPER::log($level,"[".$self->log_time." - $$] $msg");
print(STDERR "[".strftime('%F %T',localtime)." - $$] $msg\n");
# Check if we need to log this
if ($level <= $self->{'level'}) {
local *FH = $self->{'handle'};
printf(FH "[%s - %s] %s\n",strftime('%F %T',localtime),$$,$msg);
}
}
# Set log file & open it
sub open
{
my ($self, $file) = @_;
# Try open logfile
my $fh;
open($fh,">>",$file)
or die("Failed to open log file '$file': $!");
# Make sure its flushed
$fh->autoflush();
# And set it
$self->{'handle'} = $fh;
}
# Set log level
sub setLevel
{
my ($self, $level) = @_;
# And set it
$self->{'level'} = $level;
}
1;
# vim: ts=4
# OpenTrafficShaper Plugin Handler
# Copyright (C) 2007-2023, 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;
use strict;
use warnings;
use opentrafficshaper::logger;
# Exporter stuff
require Exporter;
our (@ISA,@EXPORT,@EXPORT_OK);
@ISA = qw(Exporter);
@EXPORT = qw(
isPluginLoaded
);
@EXPORT_OK = qw(
plugin_register
);
# Our own copy of the globals
my $globals;
# Check if a plugin is loaded
sub isPluginLoaded
{
my $pluginName = shift;
return defined($globals->{'plugins'}->{$pluginName});
}
# Function to register a plugin
sub plugin_register
{
my ($plugin,$systemPlugin) = @_;
# Setup our environment
my $logger = $globals->{'logger'};
# Package components
my @components = split(/\//,$plugin);
my $packageName = join("::",@components);
my $pluginName = pop(@components);
# System plugins are in the top dir
my $package;
if ($systemPlugin) {
$package = sprintf("opentrafficshaper::plugins::%s",$packageName);
} else {
$package = sprintf("opentrafficshaper::plugins::%s::%s",$packageName,$pluginName);
}
# Core configuration manager
my $res = eval("
use $package;
_plugin_register(\$plugin,\$opentrafficshaper::plugins::${packageName}::pluginInfo);
");
if ($@ || (defined($res) && $res != 0)) {
if ($@ || (defined($res) && $res != 0)) {
# Check if the error is critical or not
if ($systemPlugin) {
$logger->log(LOG_ERR,"[PLUGINS] Error loading plugin '%s', things WILL BREAK! (%s)",$pluginName,$@);
exit;
} else {
$logger->log(LOG_WARN,"[PLUGINS] Error loading plugin '%s' (%s)",$pluginName,$@);
exit;
}
} else {
$logger->log(LOG_DEBUG,"[PLUGINS] Plugin '%s' loaded.",$pluginName);
}
}
}
# Setup our main config ref
sub init
{
my $globalsref = shift;
$globals = $globalsref;
}
#
# Internal functions
#
# Register plugin info
sub _plugin_register {
my ($pluginName,$pluginInfo) = @_;
# Setup our environment
my $logger = $globals->{'logger'};
# If no info, return
if (!defined($pluginInfo)) {
$logger->log(LOG_WARN,"[MAIN] Plugin info not found for plugin => %s",$pluginName);
return -1;
}
# Check Requires
if (defined($pluginInfo->{'Requires'})) {
# Loop with plugin requires
foreach my $require (@{$pluginInfo->{'Requires'}}) {
# Check if plugin is loaded
my $found = isPluginLoaded($require);
# If still not found ERR out
if (!$found) {
$logger->log(LOG_ERR,"[MAIN] Dependency '%s' for plugin '%s' NOT MET. Make sure its loaded before '%s'",
$require,
$pluginName,
$pluginName
);
last;
}
}
}
my $res = 1;
# If we should, init the module
if (defined($pluginInfo->{'Init'})) {
if (my $res = $pluginInfo->{'Init'}($globals)) {
# Set real module name & save
$pluginInfo->{'Plugin'} = $pluginName;
$globals->{'plugins'}->{$pluginName} = $pluginInfo;
} else {
$logger->log(LOG_ERR,"[MAIN] Intialization of plugin failed => %s",$pluginName);
}
}
return 0;
}
1;
# vim: ts=4
# OpenTrafficShaper configuration manager
# Copyright (C) 2007-2013, AllWorldIT
#
# Copyright (C) 2007-2023, 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
......@@ -21,11 +21,28 @@ package opentrafficshaper::plugins::configmanager;
use strict;
use warnings;
use POE;
use Storable qw(
dclone
);
use Time::HiRes qw(
gettimeofday
tv_interval
);
use awitpt::util qw(
isNumber ISNUMBER_ALLOW_ZERO
isUsername ISUSERNAME_ALLOW_ATSIGN
prettyUndef
getHashChanges
);
use opentrafficshaper::constants;
use opentrafficshaper::logger;
use opentrafficshaper::util qw(
isIPv46 isIPv46CIDR
);
......@@ -36,188 +53,4003 @@ our (@ISA,@EXPORT,@EXPORT_OK);
@EXPORT = qw(
);
@EXPORT_OK = qw(
createGroup
isGroupIDValid
createTrafficClass
getTrafficClass
getTrafficClasses
getInterfaceTrafficClass
getAllTrafficClasses
isTrafficClassIDValid
isInterfaceIDValid
createInterface
createInterfaceClass
createInterfaceGroup
changeInterfaceTrafficClass
getEffectiveInterfaceTrafficClass2
isInterfaceTrafficClassValid
setInterfaceTrafficClassShaperState
unsetInterfaceTrafficClassShaperState
createLimit
getPoolOverride
getPoolOverrides
createPool
removePool
changePool
getPools
getPool
getPoolByName
getPoolTxInterface
getPoolRxInterface
getPoolTrafficClassID
setPoolAttribute
getPoolAttribute
removePoolAttribute
getPoolShaperState
setPoolShaperState
unsetPoolShaperState
isPoolIDValid
isPoolOverridden
isPoolReady
getEffectivePool
createPoolMember
removePoolMember
changePoolMember
getPoolMembers
getPoolMember
getPoolMemberByUsernameIP
getAllPoolMembersByInterfaceGroupIP
getPoolMemberMatchPriority
setPoolMemberShaperState
unsetPoolMemberShaperState
getPoolMemberShaperState
getPoolMemberMatchPriority
setPoolMemberAttribute
getPoolMemberAttribute
removePoolMemberAttribute
isPoolMemberReady
getTrafficClassPriority
getTrafficDirection
getInterface
getInterfaces
getInterfaceDefaultPool
getInterfaceLimit
getInterfaceGroup
getInterfaceGroups
isInterfaceGroupIDValid
getMatchPriorities
isMatchPriorityIDValid
);
use constant {
VERSION => '0.0.1',
TIMEOUT_EXPIRE_OFFLINE => 60,
VERSION => '1.0.0',
# After how long does a limit get removed if its's deemed offline
TIMEOUT_EXPIRE_OFFLINE => 10,
# How often our config check ticks
TICK_PERIOD => 5,
# Intervals for periodic actions
CLEANUP_INTERVAL => 300,
STATE_SYNC_INTERVAL => 300,
};
# Mandatory pool attributes
sub POOL_REQUIRED_ATTRIBUTES {
qw(
Name
InterfaceGroupID
TrafficClassID TxCIR RxCIR
Source
)
}
# Pool attributes that can be changed
sub POOL_CHANGE_ATTRIBUTES {
qw(
FriendlyName
TrafficClassID TxCIR RxCIR TxLimit RxLimit
Expires
Notes
)
}
# Pool persistent attributes
sub POOL_PERSISTENT_ATTRIBUTES {
qw(
ID
Name
FriendlyName
InterfaceGroupID
TrafficClassID TxCIR RxCIR TxLimit RxLimit
Expires
Source
Notes
)
}
# Class attributes that can be changed (overridden)
sub CLASS_CHANGE_ATTRIBUTES {
qw(
CIR Limit
)
}
# Class attributes that can be overidden
sub CLASS_OVERRIDE_CHANGESET_ATTRIBUTES {
qw(
CIR Limit
)
}
# Class attributes that can be overidden
sub CLASS_OVERRIDE_PERSISTENT_ATTRIBUTES {
qw(
InterfaceID
TrafficClassID
CIR Limit
)
}
# Mandatory pool member attributes
sub POOLMEMBER_REQUIRED_ATTRIBUTES {
qw(
Username IPAddress
MatchPriorityID
PoolID
GroupID
Source
)
}
# Pool member attributes that can be changed
sub POOLMEMBER_CHANGE_ATTRIBUTES {
qw(
FriendlyName
Expires
Notes
)
}
# Pool member persistent attributes
sub POOLMEMBER_PERSISTENT_ATTRIBUTES {
qw(
FriendlyName
Username IPAddress
IPNATAddress
IPNATInbound
MatchPriorityID
PoolID
GroupID
Source
Expires
Notes
)
}
# Mandatory limit attributes
sub LIMIT_REQUIRED_ATTRIBUTES {
qw(
Username IPAddress
InterfaceGroupID MatchPriorityID
GroupID
TrafficClassID TxCIR RxCIR
Source
)
}
# Pool override match attributes, one is required
sub POOL_OVERRIDE_MATCH_ATTRIBUTES {
qw(
PoolName Username IPAddress
GroupID
)
}
# Pool override attributes
sub POOL_OVERRIDE_ATTRIBUTES {
qw(
FriendlyName
PoolName Username IPAddress GroupID
TrafficClassID TxCIR RxCIR TxLimit RxLimit
Expires
Notes
)
}
# Pool override attributes that can be changed
sub POOL_OVERRIDE_CHANGE_ATTRIBUTES {
qw(
FriendlyName
TrafficClassID TxCIR RxCIR TxLimit RxLimit
Expires
Notes
)
}
# Pool override changeset attributes
sub POOL_OVERRIDE_CHANGESET_ATTRIBUTES {
qw(
TrafficClassID TxCIR RxCIR TxLimit RxLimit
)
}
# Pool override attributes supported for persistent storage
sub POOL_OVERRIDE_PERSISTENT_ATTRIBUTES {
qw(
FriendlyName
PoolName Username IPAddress GroupID
TrafficClassID TxCIR RxCIR TxLimit RxLimit
Notes
Expires Created
Source
LastUpdate
)
}
# Plugin info
our $pluginInfo = {
Name => "Config Manager",
Version => VERSION,
Init => \&init,
Init => \&plugin_init,
Start => \&plugin_start,
};
# Copy of system globals
# This modules globals
my $globals;
# System logger
my $logger;
# Pending changes
my $changeQueue = { };
# UserID counter
my $userIDMap = {};
my $userIDCounter = 1;
# Configuration for this plugin
our $config = {
# Match priorities
'match_priorities' => {
1 => 'First',
2 => 'Default',
3 => 'Fallthrough'
},
# State file
'statefile' => '/var/lib/opentrafficshaper/configmanager.state',
};
#
# GROUPS - pool members are linked to groups
#
# Attributes:
# * ID
# * Name
#
# $globals->{'Groups'}
#
# CLASSES
#
# Attributes:
# * ID
# * Name
#
# $globals->{'TrafficClasses'}
#
# INTERFACES
#
# Attributes:
# * ID
# * Name
# * Interface
# * Limit
#
# $globals->{'Interfaces'}
#
# POOLS
#
# Parameters:
# * ID
# * FriendlyName
# - Used for display purposes
# * Name
# - Unix timestamp when this entry expires, 0 if never
# * TrafficClassID
# - Traffic class ID
# * InterfaceGroupID
# - Interface group this pool is attached to
# * TxCIR
# - Traffic limit in kbps
# * RxCIR
# - Traffic limit in kbps
# * TxLimit
# - Traffic bursting limit in kbps
# * RxLimit
# - Traffic bursting limit in kbps
# * Notes
# - Notes on this limit
# * Source
# - This is the source of the limit, typically plugin.ModuleName
#
# $globals->{'Pools'}
# $globals->{'PoolNameMap'}
# $globals->{'PoolIDCounter'}
#
# POOL MEMBERS
#
# Supoprted user attributes:
# * ID
# * PoolID
# - Pool ID
# * Username
# - Users username
# * IPAddress
# - Users IP address
# * GroupID
# - Group ID
# * MatchPriorityID
# - Match priority on the backend of this limit
# * TrafficClassID
# - Class ID
# * Expires
# - Unix timestamp when this entry expires, 0 if never
# * FriendlyName
# - Used for display purposes instead of username if specified
# * Notes
# - Notes on this limit
# * Status
# - new
# - offline
# - online
# - unknown
# * Source
# - This is the source of the limit, typically plugin.ModuleName
#
# $globals->{'PoolMembers'}
# $globals->{'PoolMemberIDCounter'}
# $globals->{'PoolMemberMap'}
#
# POOL OVERRIDES
#
# Selection criteria:
# * PoolName
# - Pool name
# * Username
# - Users username
# * IPAddress
# - Users IP address
# * GroupID
# - Group ID
#
# Pool Overrides:
# * TrafficClassID
# - Class ID
# * TxCIR
# - Traffic limit in kbps
# * RxCIR
# - Traffic limit in kbps
# * TxLimit
# - Traffic bursting limit in kbps
# * RxLimit
# - Traffic bursting limit in kbps
#
# Parameters:
# * ID
# * FriendlyName
# - Used for display purposes
# * Expires
# - Unix timestamp when this entry expires, 0 if never
# * Notes
# - Notes on this limit
# * Source
# - This is the source of the limit, typically plugin.ModuleName
#
# $globals->{'PoolOverrides'}
# $globals->{'PoolOverrideIDCounter'}
#
# CHANGE QUEUES
#
# $globals->{'PoolChangeQueue'}
# $globals->{'PoolMemberChangeQueue'}
# Initialize plugin
sub init
sub plugin_init
{
$globals = shift;
my $system = shift;
my $now = time();
# Setup our environment
$logger = $globals->{'logger'};
$logger = $system->{'logger'};
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] OpenTrafficShaper Config Manager v%s - Copyright (c) 2007-2014, AllWorldIT",
VERSION
);
# Initialize
$globals->{'LastCleanup'} = $now;
$globals->{'StateChanged'} = 0;
$globals->{'LastStateSync'} = $now;
$globals->{'Groups'} = { };
$globals->{'TrafficClasses'} = { };
$globals->{'Interfaces'} = { };
$globals->{'InterfaceGroups'} = { };
$globals->{'Pools'} = { };
$globals->{'PoolNameMap'} = { };
$globals->{'PoolIDCounter'} = 1;
$globals->{'DefaultPool'} = undef;
$globals->{'PoolMembers'} = { };
$globals->{'PoolMemberIDCounter'} = 1;
$globals->{'PoolMemberMap'} = { };
$globals->{'PoolOverrides'} = { };
$globals->{'PoolOverrideIDCounter'} = 1;
$globals->{'InterfaceTrafficClasses'} = { };
$globals->{'InterfaceTrafficClassCounter'} = 1;
$globals->{'PoolChangeQueue'} = { };
$globals->{'PoolMemberChangeQueue'} = { };
$globals->{'InterfaceTrafficClassChangeQueue'} = { };
# If we have global config, use it
my $gconfig = { };
if (defined($system->{'file.config'}->{'shaping'})) {
$gconfig = $system->{'file.config'}->{'shaping'};
}
# Split off groups to load
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Loading traffic groups...");
# Check if we loaded an array or just text
my @groups;
if (defined($gconfig->{'group'})) {
if (ref($gconfig->{'group'}) eq "ARRAY") {
@groups = @{$gconfig->{'group'}};
} else {
@groups = ( $gconfig->{'group'} );
}
} else {
@groups = ( "1:Default (auto)" );
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] No groups, setting up defaults");
}
# Loop with groups
foreach my $group (@groups) {
# Skip comments
next if ($group =~ /^\s*#/);
# Split off group ID and group name
my ($groupID,$groupName) = split(/:/,$group);
if (!defined($groupID) || int($groupID) < 1) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Traffic group definition '%s' has invalid ID, ignoring",$group);
next;
}
if (!defined($groupName) || $groupName eq "") {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Traffic group definition '%s' has invalid name, ignoring",$group);
next;
}
# Create group
$groupID = createGroup({
'ID' => $groupID,
'Name' => $groupName
});
if (defined($groupID)) {
$logger->log(LOG_INFO,"[CONFIGMANAGER] Loaded traffic group '%s' [%s]",$groupName,$groupID);
}
}
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Traffic groups loaded");
# Split off traffic classes
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Loading traffic classes...");
# Check if we loaded an array or just text
my @classes;
if (defined($gconfig->{'class'})) {
if (ref($gconfig->{'class'}) eq "ARRAY") {
@classes = @{$gconfig->{'class'}};
} else {
@classes = ( $gconfig->{'class'} );
}
} else {
@classes = ( "1:Default (auto)" );
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] No classes, setting up defaults");
}
# Loop with classes
foreach my $class (@classes) {
# Skip comments
next if ($class =~ /^\s*#/);
# Split off class ID and class name
my ($trafficClassID,$className) = split(/:/,$class);
if (!defined(isNumber($trafficClassID))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Traffic class definition '%s' has invalid ID, ignoring",$class);
next;
}
if (!defined($className) || $className eq "") {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Traffic class definition '%s' has invalid name, ignoring",$class);
next;
}
# Create class
$trafficClassID = createTrafficClass({
'ID' => $trafficClassID,
'Name' => $className
});
if (!defined($trafficClassID)) {
next;
}
$logger->log(LOG_INFO,"[CONFIGMANAGER] Loaded traffic class '%s' [%s]",$className,$trafficClassID);
}
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Traffic classes loaded");
# Load interfaces
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Loading interfaces...");
my @interfaces;
if (defined($system->{'file.config'}->{'shaping.interface'})) {
@interfaces = keys %{$system->{'file.config'}->{'shaping.interface'}};
} else {
@interfaces = ( "eth0", "eth1" );
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] No interfaces defined, using 'eth0' and 'eth1'");
}
# Loop with interface
foreach my $interface (@interfaces) {
# This is the interface config to make things easier for us
my $iconfig = { };
# Check if its defined
if (defined($system->{'file.config'}->{'shaping.interface'}) &&
defined($system->{'file.config'}->{'shaping.interface'}->{$interface})
) {
$iconfig = $system->{'file.config'}->{'shaping.interface'}->{$interface}
}
# Check our friendly name for this interface
my $interfaceName = "$interface (auto)";
if (defined($iconfig->{'name'}) && $iconfig->{'name'} ne "") {
$interfaceName = $iconfig->{'name'};
} else {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Interface '%s' has no 'name' attribute, using '%s (auto)'",
$interface,$interface
);
}
# Check our interface rate
my $interfaceLimit = 100000;
if (defined($iconfig->{'rate'}) && $iconfig->{'rate'} ne "") {
# Check limit is valid
if (defined(my $rate = isNumber($iconfig->{'rate'}))) {
$interfaceLimit = $rate;
} else {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Interface '%s' has invalid 'rate' attribute, using 100000 instead",
$interface
);
}
} else {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Interface '%s' has no 'rate' attribute specified, using 100000",$interface);
}
# Create interface
my $interfaceID = createInterface({
'ID' => $interface,
'Name' => $interfaceName,
'Device' => $interface,
'Limit' => $interfaceLimit
});
# Check if we have a section in our
if (defined($iconfig->{'class_rate'})) {
# Lets pull off the class_rate items
my @iclasses;
if (ref($iconfig->{'class_rate'}) eq "ARRAY") {
@iclasses = @{$iconfig->{'class_rate'}};
} else {
@iclasses = ( $iconfig->{'class_rate'} );
}
# Loop with class_rates and parse
foreach my $iclass (@iclasses) {
# Skip comments
next if ($iclass =~ /^\s*#/);
# Split off class ID and class name
my ($itrafficClassID,$iclassCIR,$iclassLimit) = split(/[:\/]/,$iclass);
if (!defined($itrafficClassID = isTrafficClassIDValid($itrafficClassID))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Interface '%s' class definition '%s' has invalid Class ID, ignoring ".
"definition",
$interface,
$iclass
);
next;
}
# If the CIR is defined, try use it
if (defined($iclassCIR)) {
# If its not a number, something is wrong
if ($iclassCIR =~ /^([1-9][0-9]*)(%)?$/) {
my ($cir,$percent) = ($1,$2);
# Check if this is a percentage or an actual kbps value
if (defined($percent)) {
$iclassCIR = int($interfaceLimit * ($cir / 100));
} else {
$iclassCIR = $cir;
}
} else {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Interface '%s' class '%s' has invalid CIR, ignoring definition",
$interface,
$itrafficClassID
);
next;
}
} else {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Interface '%s' class '%s' has missing CIR, ignoring definition",
$interface,
$itrafficClassID
);
next;
}
# If the limit is defined, try use it
if (defined($iclassLimit)) {
# If its not a number, something is wrong
if ($iclassLimit =~ /^([1-9][0-9]*)(%)?$/) {
my ($Limit,$percent) = ($1,$2);
# Check if this is a percentage or an actual kbps value
if (defined($percent)) {
$iclassLimit = int($interfaceLimit * ($Limit / 100));
} else {
$iclassLimit = $Limit;
}
} else {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Interface '%s' class '%s' has invalid Limit, ignoring",
$interface,
$itrafficClassID
);
next;
}
} else {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Interface '%s' class '%s' has missing Limit, using CIR '%s' instead",
$interface,
$itrafficClassID,
$iclassCIR
);
$iclassLimit = $iclassCIR;
}
# Check if rates are below are sane
if ($iclassCIR > $interfaceLimit) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Interface '%s' class '%s' has CIR '%s' > interface speed '%s', ".
"adjusting to '%s'",
$interface,
$itrafficClassID,
$iclassCIR,
$interfaceLimit,
$interfaceLimit
);
$iclassCIR = $interfaceLimit;
}
if ($iclassLimit > $interfaceLimit) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Interface '%s' class '%s' has Limit '%s' > interface speed '%s', ".
"adjusting to '%s'",
$interface,
$itrafficClassID,
$iclassCIR,
$interfaceLimit,
$interfaceLimit
);
$iclassLimit = $interfaceLimit;
}
if ($iclassCIR > $iclassLimit) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Interface '%s' class '%s' has CIR '%s' > Limit '%s', adjusting CIR ".
"to '%s'",
$interface,
$itrafficClassID,
$iclassLimit,
$iclassLimit,
$iclassLimit
);
$iclassCIR = $iclassLimit;
}
# Create class
my $interfaceTrafficClassID = createInterfaceTrafficClass({
'InterfaceID' => $interfaceID,
'TrafficClassID' => $itrafficClassID,
'CIR' => $iclassCIR,
'Limit' => $iclassLimit
});
if (!defined($interfaceTrafficClassID)) {
next;
}
$logger->log(LOG_INFO,"[CONFIGMANAGER] Loaded interface '%s' class rate for class ID '%s': %s/%s",
$interface,
$itrafficClassID,
$iclassCIR,
$iclassLimit
);
}
}
# Time to check the interface classes
foreach my $trafficClassID (getAllTrafficClasses()) {
# Check if we have a rate defined for this class in the interface definition
if (!isInterfaceTrafficClassValid($interfaceID,$trafficClassID)) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Interface '%s' has no class '%s' defined, using interface limit",
$interface,
$trafficClassID
);
# Create the default class
createInterfaceTrafficClass({
'InterfaceID' => $interfaceID,
'TrafficClassID' => $trafficClassID,
'CIR' => $interfaceLimit,
'Limit' => $interfaceLimit
});
$logger->log(LOG_INFO,"[CONFIGMANAGER] Loaded interface '%s' default class rate for class ID '%s': %s/%s",
$interface,
$trafficClassID,
$interfaceLimit,
$interfaceLimit
);
}
}
}
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Loading interfaces completed");
# Pull in interface groupings
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Loading interface groups...");
# Check if we loaded an array or just text
my @cinterfaceGroups;
if (defined($gconfig->{'interface_group'})) {
if (ref($gconfig->{'interface_group'}) eq "ARRAY") {
@cinterfaceGroups = @{$gconfig->{'interface_group'}};
} else {
@cinterfaceGroups = ( $gconfig->{'interface_group'} );
}
} else {
@cinterfaceGroups = ( "eth1,eth0:Default" );
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] No interface groups, trying default eth1,eth0");
}
# Loop with interface groups
foreach my $interfaceGroup (@cinterfaceGroups) {
# Skip comments
next if ($interfaceGroup =~ /^\s*#/);
# Split off class ID and class name
my ($txInterface,$rxInterface,$friendlyName) = split(/[:,]/,$interfaceGroup);
if (!isInterfaceIDValid($txInterface)) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Interface group definition '%s' has invalid interface '%s', ignoring",
$interfaceGroup,
$txInterface
);
next;
}
if (!isInterfaceIDValid($rxInterface)) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Interface group definition '%s' has invalid interface '%s', ignoring",
$interfaceGroup,
$rxInterface
);
next;
}
if (!defined($friendlyName) || $friendlyName eq "") {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Interface group definition '%s' has invalid friendly name, ignoring",
$interfaceGroup,
);
next;
}
# Create interface group
my $interfaceGroupID = createInterfaceGroup({
'Name' => $friendlyName,
'TxInterface' => $txInterface,
'RxInterface' => $rxInterface
});
if (!defined($interfaceGroupID)) {
next;
}
$logger->log(LOG_INFO,"[CONFIGMANAGER] Loaded interface group '%s' [%s] with interfaces '%s/%s'",
$friendlyName,
$interfaceGroupID,
$txInterface,
$rxInterface
);
}
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Interface groups loaded");
# Check if we using a default pool or not
if (defined($gconfig->{'default_pool'})) {
# Check if its a number
if (defined(my $default_pool = isNumber($gconfig->{'default_pool'}))) {
if (isTrafficClassIDValid($default_pool)) {
$logger->log(LOG_INFO,"[CONFIGMANAGER] Default pool set to use class '%s'",
$default_pool
);
$globals->{'DefaultPool'} = $default_pool;
} else {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot enable default pool, class '%s' does not exist",
$default_pool
);
}
} else {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot enable default pool, value for 'default_pool' is invalid");
}
}
# Check if we have a state file
if (defined(my $statefile = $system->{'file.config'}->{'system'}->{'statefile'})) {
$config->{'statefile'} = $statefile;
$logger->log(LOG_INFO,"[CONFIGMANAGER] Set statefile to '%s'",$statefile);
}
# This is our configuration processing session
POE::Session->create(
inline_states => {
_start => \&session_init,
tick => \&session_tick,
process_change => \&process_change,
_start => \&_session_start,
_stop => \&_session_stop,
_tick => \&_session_tick,
_SIGHUP => \&_session_SIGHUP,
limit_add => \&_session_limit_add,
pool_override_add => \&_session_pool_override_add,
pool_override_change => \&_session_pool_override_change,
pool_override_remove => \&_session_pool_override_remove,
pool_add => \&_session_pool_add,
pool_remove => \&_session_pool_remove,
pool_change => \&_session_pool_change,
poolmember_add => \&_session_poolmember_add,
poolmember_remove => \&_session_poolmember_remove,
poolmember_change => \&_session_poolmember_change,
}
);
}
# Start the plugin
sub plugin_start
{
# Load config
if (-f $config->{'statefile'}) {
_load_statefile();
} else {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Statefile '%s' cannot be opened: %s",$config->{'statefile'},$!);
}
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] OpenTrafficShaper Config Manager v".VERSION." - Copyright (c) 2013, AllWorldIT")
$logger->log(LOG_INFO,"[CONFIGMANAGER] Started with %s pools, %s pool members and %s pool overrides",
scalar(keys %{$globals->{'Pools'}}),
scalar(keys %{$globals->{'PoolMembers'}}),
scalar(keys %{$globals->{'PoolOverrides'}})
);
}
# Initialize config manager
sub session_init {
my $kernel = $_[KERNEL];
sub _session_start
{
my ($kernel,$heap) = @_[KERNEL,HEAP];
# Set our alias
$kernel->alias_set("configmanager");
# Set delay on config updates
$kernel->delay(tick => 5);
$kernel->delay('_tick' => TICK_PERIOD);
$kernel->sig('HUP', '_SIGHUP');
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Initialized");
}
# Stop the session
sub _session_stop
{
my ($kernel,$heap) = @_[KERNEL,HEAP];
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Shutting down, saving configuration...");
# We only need to write the sate if something changed?
if ($globals->{'StateChanged'}) {
# The 1 means FULL WRITE of all entries
_write_statefile(1);
}
# Blow away all data
$globals = undef;
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Shutdown");
$logger = undef;
}
# Time ticker for processing changes
sub session_tick {
sub _session_tick
{
my $kernel = $_[KERNEL];
# Suck in global
my $users = $globals->{'users'};
# Now
my $now = time();
# Check if we should sync state to disk
if ($globals->{'StateChanged'} && $globals->{'LastStateSync'} + STATE_SYNC_INTERVAL < $now) {
_write_statefile();
}
# Loop with changes
foreach my $uid (keys %{$changeQueue}) {
# Global user
my $guser = $users->{$uid};
# Change user
my $cuser = $changeQueue->{$uid};
# Check if we should cleanup
if ($globals->{'LastCleanup'} + CLEANUP_INTERVAL < $now) {
# Loop with all pool overrides and check for expired entries
while (my ($poid, $poolOverride) = each(%{$globals->{'PoolOverrides'}})) {
# Pool override has effectively expired
if (defined($poolOverride->{'Expires'}) && $poolOverride->{'Expires'} > 0 && $poolOverride->{'Expires'} < $now) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Pool override '%s' [%s] has expired, removing",
$poolOverride->{'FriendlyName'},
$poid
);
removePoolOverride($poid);
}
}
# Loop with all pool members and check for expired entries
while (my ($pmid, $poolMember) = each(%{$globals->{'PoolMembers'}})) {
# Pool member has effectively expired
if (defined($poolMember->{'Expires'}) && $poolMember->{'Expires'} > 0 && $poolMember->{'Expires'} < $now) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Pool member '%s' [%s] has expired, removing",
$poolMember->{'Username'},
$pmid
);
removePoolMember($pmid);
}
}
# Loop with all the pools and check for expired entries
while (my ($pid, $pool) = each(%{$globals->{'Pools'}})) {
# Pool has effectively expired
if (defined($pool->{'Expires'}) && $pool->{'Expires'} > 0 && $pool->{'Expires'} < $now) {
# There are no members, its safe to remove
if (getPoolMembers($pid) == 0) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Pool '%s' [%s] has expired, removing",
$pool->{'Name'},
$pid
);
removePool($pid);
}
}
}
# Reset last cleanup time
$globals->{'LastCleanup'} = $now;
}
# NO USER IN LIST
if (!defined($guser)) {
# Loop through interface traffic classes
while (my ($interfaceTrafficClassID, $interfaceTrafficClass) = each(%{$globals->{'InterfaceTrafficClassChangeQueue'}})) {
my $shaperState = getInterfaceTrafficClassShaperState($interfaceTrafficClassID);
# NO USER IN LIST => CHANGE IS NEW or ONLINE
if (($cuser->{'Status'} eq "new" || $cuser->{'Status'} eq "online")) {
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Processing new user '$cuser->{'Username'}' [$uid]");
# This is now live
$users->{$uid} = $cuser;
$users->{$uid}->{'shaper_live'} = SHAPER_PENDING;
# Post to shaper
$kernel->post("shaper" => "add" => $uid);
# Traffic class has been changed
if ($interfaceTrafficClass->{'Status'} == CFGM_CHANGED) {
# If the shaper is live we can go ahead
if ($shaperState & SHAPER_LIVE) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Interface traffic class [%s] has been modified, sending to shaper",
$interfaceTrafficClassID
);
$kernel->post('shaper' => 'class_change' => $interfaceTrafficClassID);
# Set pending online
setInterfaceTrafficClassShaperState($interfaceTrafficClassID,SHAPER_PENDING);
$interfaceTrafficClass->{'Status'} = CFGM_ONLINE;
# Remove from queue
delete($globals->{'InterfaceTrafficClassChangeQueue'}->{$interfaceTrafficClassID});
# NO USER IN LIST => CHANGE IS OFFLINE OR UNKNOWN
} else {
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Ignoring user '$cuser->{'Username'}' [$uid] state '$cuser->{'Status'}', user was not online");
$logger->log(LOG_ERR,"[CONFIGMANAGER] Interface traffic class [%s] has UNKNOWN state '%s'",
$interfaceTrafficClassID,
$shaperState
);
}
# Remove from change queue
delete($changeQueue->{$uid});
# USER IN LIST
} else {
# USER IN LIST => CHANGE IS NEW
if ($cuser->{'Status'} eq "new") {
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] User '$cuser->{'Username'}' [$uid], user live but new connection?");
$logger->log(LOG_ERR,"[CONFIGMANAGER] Interface traffic class [%s] has UNKNOWN status '%s'",
$interfaceTrafficClassID,
$interfaceTrafficClass->{'Status'}
);
}
}
# Remove from change queue
delete($changeQueue->{$uid});
# Loop through pool change queue
while (my ($pid, $pool) = each(%{$globals->{'PoolChangeQueue'}})) {
my $shaperState = getPoolShaperState($pid);
# USER IN LIST => CHANGE IS ONLINE
} elsif ($cuser->{'Status'} eq "online") {
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] User '$cuser->{'Username'}' [$uid], user in list, new online notification");
# Pool is newly added
if ($pool->{'Status'} == CFGM_NEW) {
# Remove from change queue
delete($changeQueue->{$uid});
# If the change is not yet live, we should queue it to go live
if ($shaperState & SHAPER_NOTLIVE) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Pool '%s' [%s] new and is not live, adding to shaper",
$pool->{'Name'},
$pid
);
$kernel->post('shaper' => 'pool_add' => $pid);
# Set pending online
setPoolShaperState($pid,SHAPER_PENDING);
$pool->{'Status'} = CFGM_ONLINE;
# Remove from queue
delete($globals->{'PoolChangeQueue'}->{$pid});
} else {
$logger->log(LOG_ERR,"[CONFIGMANAGER] Pool '%s' [%s] has UNKNOWN state '%s'",
$pool->{'Name'},
$pid,
$shaperState
);
}
# Pool is online but NOTLIVE
} elsif ($pool->{'Status'} == CFGM_ONLINE) {
# We've transitioned more than likely from offline, any state to online
# We don't care if the shaper is pending removal, we going to force re-adding now
if (!($shaperState & SHAPER_LIVE)) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Pool '%s' [%s] online and is not live, re-queue as add",
$pool->{'Name'},
$pid
);
$pool->{'Status'} = CFGM_NEW;
} else {
$logger->log(LOG_ERR,"[CONFIGMANAGER] Pool '%s' [%s] has UNKNOWN state '%s'",
$pool->{'Name'},
$pid,
$shaperState
);
}
# USER IN LIST => CHANGE IS OFFLINE
} elsif ($cuser->{'Status'} eq "offline") {
# USER IN LIST => CHANGE IS OFFLINE => TIMEOUT EXPIRED
if ($now - $cuser->{'LastUpdate'} > TIMEOUT_EXPIRE_OFFLINE) {
# Pool has been modified
} elsif ($pool->{'Status'} == CFGM_CHANGED) {
# If the shaper is live we can go ahead
if ($shaperState & SHAPER_LIVE) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Pool '%s' [%s] has been modified, sending to shaper",
$pool->{'Name'},
$pid
);
$kernel->post('shaper' => 'pool_change' => $pid);
# Set pending online
setPoolShaperState($pid,SHAPER_PENDING);
$pool->{'Status'} = CFGM_ONLINE;
# Remove from queue
delete($globals->{'PoolChangeQueue'}->{$pid});
# Remove entry if no longer live
if (!$guser->{'shaper_live'}) {
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] User '$cuser->{'Username'}' [$uid], user in list, but offline now, expired and not live on shaper");
} elsif ($shaperState & SHAPER_NOTLIVE) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Pool '%s' [%s] has been modified and is not live, re-queue as add",
$pool->{'Name'},
$pid
);
$pool->{'Status'} = CFGM_NEW;
# Remove from system
delete($users->{$uid});
# Remove from change queue
delete($changeQueue->{$uid});
} else {
$logger->log(LOG_ERR,"[CONFIGMANAGER] Pool '%s' [%s] has UNKNOWN state '%s'",
$pool->{'Name'},
$pid,
$shaperState
);
}
# Push to shaper
} elsif ($guser->{'status'} ne "offline") {
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] User '$cuser->{'Username'}' [$uid], user in list, but offline now and expired, still live on shaper");
# Post to shaper
$kernel->post("shaper" => "remove" => $uid);
# Update that we're offline
$guser->{'Status'} = 'offline';
# Pool is being removed?
} elsif ($pool->{'Status'} == CFGM_OFFLINE) {
# If the change is live, but should go offline, queue it
if ($shaperState & SHAPER_LIVE) {
if ($now - $pool->{'LastUpdate'} > TIMEOUT_EXPIRE_OFFLINE) {
# If we still have pool members, we got to abort
if (!getPoolMembers($pid)) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Pool '%s' [%s] marked offline and expired, removing from shaper",
$pool->{'Name'},
$pid
);
$kernel->post('shaper' => 'pool_remove' => $pid);
setPoolShaperState($pid,SHAPER_PENDING);
} else {
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] User '$cuser->{'Username'}' [$uid], user in list, but offline now and expired, still live, waiting for shaper");
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Pool '%s' [%s] marked offline, but still has pool members, ".
"aborting remove",
$pool->{'Name'},
$pid
);
$pool->{'Status'} = CFGM_ONLINE;
delete($globals->{'PoolChangeQueue'}->{$pid});
}
} else {
# Try remove all our pool members
if (my @poolMembers = getPoolMembers($pid)) {
# Loop with members and remove
foreach my $pmid (@poolMembers) {
my $poolMember = $globals->{'PoolMembers'}->{$pmid};
# Only remove ones online
if ($poolMember->{'Status'} == CFGM_ONLINE) {
$logger->log(LOG_INFO,"[CONFIGMANAGER] Pool '%s' [%s] marked offline and not expired, removing ".
"pool member [%s]",
$pool->{'Name'},
$pid,
$pmid
);
removePoolMember($pmid);
}
}
}
}
} elsif ($shaperState & SHAPER_NOTLIVE) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Pool '%s' [%s] marked offline and is not live, removing",
$pool->{'Name'},
$pid
);
# Remove pool from name map
delete($globals->{'PoolNameMap'}->{$pool->{'InterfaceGroupID'}}->{$pool->{'Name'}});
# Remove pool member mapping
delete($globals->{'PoolMemberMap'}->{$pid});
# Remove from queue
delete($globals->{'PoolChangeQueue'}->{$pid});
# Cleanup pool overrides
_remove_pool_override($pid);
# Remove pool
delete($globals->{'Pools'}->{$pid});
}
# Update the last time we got an update
$guser->{'LastUpdate'} = $cuser->{'LastUpdate'};
} else {
$logger->log(LOG_ERR,"[CONFIGMANAGER] Pool '%s' [%s] has UNKNOWN status '%s'",
$pool->{'Name'},
$pid,
$pool->{'Status'}
);
}
}
# Reset tick
$kernel->delay(tick => 5);
};
# Loop through pool member change queue
while (my ($pmid, $poolMember) = each(%{$globals->{'PoolMemberChangeQueue'}})) {
my $pool = $globals->{'Pools'}->{$poolMember->{'PoolID'}};
# We need to skip doing anything until the pool becomes live
if (getPoolShaperState($pool->{'ID'}) & SHAPER_NOTLIVE) {
next;
}
my $shaperState = getPoolMemberShaperState($pmid);
# Pool member is newly added
if ($poolMember->{'Status'} == CFGM_NEW) {
# If the change is not yet live, we should queue it to go live
if ($shaperState & SHAPER_NOTLIVE) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Pool '%s' member '%s' [%s] new and is not live, adding to shaper",
$pool->{'Name'},
$poolMember->{'IPAddress'},
$pmid
);
$kernel->post('shaper' => 'poolmember_add' => $pmid);
# Set pending online
setPoolMemberShaperState($pmid,SHAPER_PENDING);
$poolMember->{'Status'} = CFGM_ONLINE;
# Remove from queue
delete($globals->{'PoolMemberChangeQueue'}->{$pmid});
} else {
$logger->log(LOG_ERR,"[CONFIGMANAGER] Pool '%s' member '%s' [%s] has UNKNOWN state '%s'",
$pool->{'Name'},
$poolMember->{'IPAddress'},
$pmid,
$shaperState
);
}
# Pool member is online but NOTLIVE
} elsif ($poolMember->{'Status'} == CFGM_ONLINE) {
# We've transitioned more than likely from offline, any state to online
# We don't care if the shaper is pending removal, we going to force re-adding now
if (!($shaperState & SHAPER_LIVE)) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Pool '%s' member '%s' [%s] online and is not live, re-queue as add",
$pool->{'Name'},
$poolMember->{'IPAddress'},
$pmid
);
$poolMember->{'Status'} = CFGM_NEW;
} else {
$logger->log(LOG_ERR,"[CONFIGMANAGER] Pool '%s' member '%s' [%s] has UNKNOWN state '%s'",
$pool->{'Name'},
$poolMember->{'IPAddress'},
$pmid,
$shaperState
);
}
# Pool member has been modified
} elsif ($poolMember->{'Status'} == CFGM_CHANGED) {
# If the shaper is live we can go ahead
if ($shaperState & SHAPER_LIVE) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Pool '%s' member '%s' [%s] has been modified, sending to shaper",
$pool->{'Name'},
$poolMember->{'IPAddress'},
$pmid
);
$kernel->post('shaper' => 'poolmember_change' => $pmid);
# Set pending online
setPoolMemberShaperState($pmid,SHAPER_PENDING);
$poolMember->{'Status'} = CFGM_ONLINE;
# Remove from queue
delete($globals->{'PoolMemberChangeQueue'}->{$pmid});
} elsif ($shaperState & SHAPER_NOTLIVE) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Pool '%s' member '%s' [%s] has been modified and is not live, re-queue as ".
"add",
$pool->{'Name'},
$poolMember->{'IPAddress'},
$pmid
);
$poolMember->{'Status'} = CFGM_NEW;
} else {
$logger->log(LOG_ERR,"[CONFIGMANAGER] Pool '%s' member '%s' [%s] has UNKNOWN state '%s'",
$pool->{'Name'},
$poolMember->{'IPAddress'},
$pmid,
$shaperState
);
}
# Pool is being removed?
} elsif ($poolMember->{'Status'} == CFGM_OFFLINE) {
# If the change is live, but should go offline, queue it
if ($shaperState & SHAPER_LIVE) {
if ($now - $poolMember->{'LastUpdate'} > TIMEOUT_EXPIRE_OFFLINE) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Pool '%s' member '%s' [%s] marked offline and expired, removing ".
"from shaper",
$pool->{'Name'},
$poolMember->{'IPAddress'},
$pmid
);
$kernel->post('shaper' => 'poolmember_remove' => $pmid);
setPoolMemberShaperState($pmid,SHAPER_PENDING);
} else {
$logger->log(LOG_INFO,"[CONFIGMANAGER] Pool '%s' member '%s' [%s] marked offline and fresh, postponing",
$pool->{'Name'},
$poolMember->{'IPAddress'},
$pmid
);
}
} elsif ($shaperState & SHAPER_NOTLIVE) {
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Pool '%s' member '%s' [%s] marked offline and is not live, removing",
$pool->{'Name'},
$poolMember->{'IPAddress'},
$pmid
);
# Unlink interface IP address map
delete($globals->{'InterfaceGroups'}->{$pool->{'InterfaceGroupID'}}->{'IPMap'}->{$poolMember->{'IPAddress'}}
->{$pmid});
# Unlink pool map
delete($globals->{'PoolMemberMap'}->{$pool->{'ID'}}->{$pmid});
# Remove from queue
delete($globals->{'PoolMemberChangeQueue'}->{$pmid});
# We need to re-process the pool overrides after the member has been removed
_resolve_pool_override([$poolMember->{'PoolID'}]);
# Remove pool member
delete($globals->{'PoolMembers'}->{$pmid});
# Check if we have/had conflicts
if ((my @conflicts = keys
%{$globals->{'InterfaceGroups'}->{$pool->{'InterfaceGroupID'}}->{'IPMap'}->{$poolMember->{'IPAddress'}}}) > 0)
{
# We can only re-tag a pool member for adding if we have 1 pool member
if (@conflicts == 1) {
# Grab conflicted pool member, its index 0 in the conflicts array
my $cPoolMember = $globals->{'PoolMembers'}->{$conflicts[0]};
my $cPoolMemberShaperState = getPoolMemberShaperState($cPoolMember->{'ID'});
# We only want to work with conflicts
if ($cPoolMemberShaperState & SHAPER_CONFLICT) {
# Grab pool
my $cPool = $globals->{'Pools'}->{$cPoolMember->{'PoolID'}};
# Unset conflict state
unsetPoolMemberShaperState($cPoolMember->{'ID'},SHAPER_CONFLICT);
# Add to change queue
$globals->{'PoolMemberChangeQueue'}->{$cPoolMember->{'ID'}} = $cPoolMember;
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] IP '%s' is no longer conflicted, removing conflict from ".
"pool '%s' member '%s' [%s], was conflicted with pool '%s' member '%s' [%s]",
$cPoolMember->{'IPAddress'},
$cPool->{'Name'},
$cPoolMember->{'Username'},
$cPoolMember->{'ID'},
$pool->{'Name'},
$poolMember->{'Username'},
$poolMember->{'ID'}
);
}
} else {
# Loop wiht conflicts and build some log items to use
my @logItems;
foreach my $pmid (@conflicts) {
my $cPoolMember = $globals->{'PoolMembers'}->{$pmid};
my $cPool = $globals->{'Pools'}->{$cPoolMember->{'PoolID'}};
push(@logItems,sprintf("Pool:%s/Member:%s",$cPool->{'Name'},$cPoolMember->{'Username'}));
}
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] IP '%s' is still in conflict: %s",
$poolMember->{'IPAddress'},
join(", ",@logItems)
);
}
}
}
} else {
$logger->log(LOG_ERR,"[CONFIGMANAGER] Pool '%s' member '%s' [%s] has UNKNOWN status '%s'",
$pool->{'Name'},
$poolMember->{'IPAddress'},
$pmid,
$poolMember->{'Status'}
);
}
}
# Reset tick
$kernel->delay('_tick' => TICK_PERIOD);
}
# Handle SIGHUP
sub _session_SIGHUP
{
my ($kernel, $heap, $signal_name) = @_[KERNEL, HEAP, ARG0];
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Got SIGHUP, ignoring for now");
}
# Event for 'pool_add'
sub _session_pool_add
{
my ($kernel, $poolData) = @_[KERNEL, ARG0];
if (!defined($poolData)) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] No pool data provided for 'pool_add' event");
return;
}
# Check if we have all the attributes we need
my $isInvalid;
foreach my $attr (POOL_REQUIRED_ATTRIBUTES) {
if (!defined($poolData->{$attr})) {
$isInvalid = $attr;
last;
}
}
if ($isInvalid) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool add as there is an attribute missing: '%s'",
$isInvalid
);
return;
}
createPool($poolData);
}
# Event for 'pool_remove'
sub _session_pool_remove
{
my ($kernel, $pid) = @_[KERNEL, ARG0];
my $pool;
if (!defined(getPool($pid))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Invalid pool ID '%s' for 'pool_remove' event",prettyUndef($pid));
return;
}
removePool($pid);
}
# Event for 'pool_change'
sub _session_pool_change
{
my ($kernel, $poolData) = @_[KERNEL, ARG0];
if (!isPoolIDValid($poolData->{'ID'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Invalid pool ID '%s' for 'pool_change' event",prettyUndef($poolData->{'ID'}));
return;
}
changePool($poolData);
}
# Event for 'poolmember_add'
sub _session_poolmember_add
{
my ($kernel, $poolMemberData) = @_[KERNEL, ARG0];
if (!defined($poolMemberData)) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] No pool member data provided for 'poolmember_add' event");
return;
}
# Check if we have all the attributes we need
my $isInvalid;
foreach my $attr (POOLMEMBER_REQUIRED_ATTRIBUTES) {
if (!defined($poolMemberData->{$attr})) {
$isInvalid = $attr;
last;
}
}
if ($isInvalid) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process poolmember add as there is an attribute missing: '%s',$isInvalid");
return;
}
createPoolMember($poolMemberData);
}
# Event for 'poolmember_remove'
sub _session_poolmember_remove
{
my ($kernel, $pmid) = @_[KERNEL, ARG0];
if (!isPoolMemberIDValid($pmid)) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Invalid pool member ID '%s' for 'poolmember_remove' event",prettyUndef($pmid));
return;
}
removePoolMember($pmid);
}
# Event for 'poolmember_change'
sub _session_poolmember_change
{
my ($kernel, $poolMemberData) = @_[KERNEL, ARG0];
if (!isPoolMemberIDValid($poolMemberData->{'ID'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Invalid pool member ID '%s' for 'poolmember_change' event",
prettyUndef($poolMemberData->{'ID'})
);
return;
}
changePoolMember($poolMemberData);
}
# Event for 'limit_add'
sub _session_limit_add
{
my ($kernel, $limitData) = @_[KERNEL, ARG0];
if (!defined($limitData)) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] No limit data provided for 'limit_add' event");
return;
}
# Check if we have all the attributes we need
my $isInvalid;
foreach my $attr (LIMIT_REQUIRED_ATTRIBUTES) {
if (!defined($limitData->{$attr})) {
$isInvalid = $attr;
last;
}
}
if ($isInvalid) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process limit add as there is an attribute missing: '%s'",$isInvalid);
return;
}
createLimit($limitData);
}
# Event for 'pool_override_add'
sub _session_pool_override_add
{
my ($kernel, $poolOverrideData) = @_[KERNEL, ARG0];
if (!defined($poolOverrideData)) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] No pool override data provided for 'pool_override_add' event");
return;
}
# Check that we have at least one match attribute
my $isValid = 0;
foreach my $item (POOL_OVERRIDE_MATCH_ATTRIBUTES) {
$isValid++;
}
if (!$isValid) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool override as there is no selection attribute");
return;
}
createPoolOverride($poolOverrideData);
}
# Event for 'pool_override_remove'
sub _session_pool_override_remove
{
my ($kernel, $poid) = @_[KERNEL, ARG0];
if (!isPoolOverrideIDValid($poid)) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Invalid pool override ID '%s' for 'pool_override_remove' event",
prettyUndef($poid)
);
return;
}
removePoolOverride($poid);
}
# Event for 'pool_override_change'
sub _session_pool_override_change
{
my ($kernel, $poolOverrideData) = @_[KERNEL, ARG0];
if (!isPoolOverrideIDValid($poolOverrideData->{'ID'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Invalid pool override ID '%s' for 'pool_override_change' event",
prettyUndef($poolOverrideData->{'ID'})
);
return;
}
changePoolOverride($poolOverrideData);
}
# Function to create a group
sub createGroup
{
my $groupData = shift;
my $group;
# Check if ID is valid
if (!defined($group->{'ID'} = $groupData->{'ID'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to add group as ID is invalid");
return;
}
# Check if Name is valid
if (!defined($group->{'Name'} = $groupData->{'Name'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to add group as Name is invalid");
return;
}
# Add pool
$globals->{'Groups'}->{$group->{'ID'}} = $group;
return $group->{'ID'};
}
# Function to check the group ID exists
sub isGroupIDValid
{
my $gid = shift;
if (!defined($globals->{'Groups'}->{$gid})) {
return;
}
return $gid;
}
# Function to create a traffic class
sub createTrafficClass
{
my $classData = shift;
my $class;
# Check if ID is valid
if (!defined($class->{'ID'} = $classData->{'ID'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to add traffic class as ID is invalid");
return;
}
# Check if Name is valid
if (!defined($class->{'Name'} = $classData->{'Name'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to add traffic class as Name is invalid");
return;
}
# Add pool
$globals->{'TrafficClasses'}->{$class->{'ID'}} = $class;
return $class->{'ID'};
}
# Function to get traffic classes
sub getTrafficClasses
{
my @trafficClasses = ( );
# Loop with traffic classes
foreach my $trafficClassID (keys %{$globals->{'TrafficClasses'}}) {
# Skip over default pool if we have one
if (defined($globals->{'DefaultPool'}) && $trafficClassID eq $globals->{'DefaultPool'}) {
next;
}
# Add to class list
push (@trafficClasses,$trafficClassID);
}
return @trafficClasses;
}
# Function to get a interface traffic class
sub getInterfaceTrafficClass
{
my ($interfaceID,$trafficClassID) = @_;
# Check if this interface ID is valid
if (!isInterfaceIDValid($interfaceID)) {
return;
}
# Check if traffic class ID is valid
if (!defined($trafficClassID = isNumber($trafficClassID,ISNUMBER_ALLOW_ZERO))) {
return;
}
if ($trafficClassID && !isTrafficClassIDValid($trafficClassID)) {
return;
}
my $interfaceTrafficClass = dclone($globals->{'Interfaces'}->{$interfaceID}->{'TrafficClasses'}->{$trafficClassID});
# Check if the traffic class ID is not 0
if ($trafficClassID) {
$interfaceTrafficClass->{'Name'} = $globals->{'TrafficClasses'}->{$trafficClassID}->{'Name'};
# If if it 0, this is a root class
} else {
$interfaceTrafficClass->{'Name'} = "Root Class";
}
delete($interfaceTrafficClass->{'.applied_overrides'});
return $interfaceTrafficClass;
}
# Function to get a interface traffic class
sub getInterfaceTrafficClass2
{
my $interfaceTrafficClassID = shift;
# Check if this interface ID is valid
if (!isInterfaceTrafficClassIDValid2($interfaceTrafficClassID)) {
return;
}
my $interfaceTrafficClass = dclone($globals->{'InterfaceTrafficClasses'}->{$interfaceTrafficClassID});
$interfaceTrafficClass->{'Name'} = $globals->{'TrafficClasses'}->{$interfaceTrafficClass->{'TrafficClassID'}};
delete($interfaceTrafficClass->{'.applied_overrides'});
return $interfaceTrafficClass;
}
# Function to check if traffic class is valid
sub isInterfaceTrafficClassValid
{
my ($interfaceID,$trafficClassID) = @_;
if (
!defined($interfaceID) || !defined($trafficClassID) ||
!defined($globals->{'Interfaces'}->{$interfaceID}) ||
!defined($globals->{'Interfaces'}->{$interfaceID}->{'TrafficClasses'}->{$trafficClassID})
) {
return;
}
return $globals->{'Interfaces'}->{$interfaceID}->{'TrafficClasses'}->{$trafficClassID}->{'ID'};
}
# Function to check the interface traffic class ID is valid
sub isInterfaceTrafficClassIDValid2
{
my $interfaceTrafficClassID = shift;
if (
!defined($interfaceTrafficClassID) ||
!defined($globals->{'InterfaceTrafficClasses'}->{$interfaceTrafficClassID})
) {
return;
}
return $interfaceTrafficClassID;
}
# Function to create an interface class
sub createInterfaceTrafficClass
{
my $interfaceTrafficClassData = shift;
my $interfaceTrafficClass;
# Check if InterfaceID is valid
if (!defined($interfaceTrafficClass->{'InterfaceID'} = isInterfaceIDValid($interfaceTrafficClassData->{'InterfaceID'}))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to add interface traffic class as InterfaceID is invalid");
return;
}
# Check if traffic class ID is valid
my $interfaceTrafficClassID;
if (!defined($interfaceTrafficClassID = isNumber($interfaceTrafficClassData->{'TrafficClassID'},ISNUMBER_ALLOW_ZERO))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process class change as there is no 'TrafficClassID' attribute");
return;
}
if ($interfaceTrafficClassID && !isTrafficClassIDValid($interfaceTrafficClassData->{'TrafficClassID'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process class change as 'TrafficClassID' attribute is invalid");
return;
}
$interfaceTrafficClass->{'TrafficClassID'} = $interfaceTrafficClassID;
# Check CIR is valid
if (!defined($interfaceTrafficClass->{'CIR'} = isNumber($interfaceTrafficClassData->{'CIR'}))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to add interface as CIR is invalid");
return;
}
# Check Limit is valid
if (!defined($interfaceTrafficClass->{'Limit'} = isNumber($interfaceTrafficClassData->{'Limit'}))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to add interface as Limit is invalid");
return;
}
# Set ID
$interfaceTrafficClass->{'ID'} = $globals->{'InterfaceTrafficClassCounter'}++;
# Set status
$interfaceTrafficClass->{'Status'} = CFGM_NEW;
# Add interface
$globals->{'Interfaces'}->{$interfaceTrafficClass->{'InterfaceID'}}->{'TrafficClasses'}
->{$interfaceTrafficClass->{'TrafficClassID'}} = $interfaceTrafficClass;
# Link to interface traffic classes
$globals->{'InterfaceTrafficClasses'}->{$interfaceTrafficClass->{'ID'}} = $interfaceTrafficClass;
# TODO: Hack, this should set NOTLIVE & NEW and have the shaper create as per note in plugin_init section
# Set status on this interface traffic class
setInterfaceTrafficClassShaperState($interfaceTrafficClass->{'ID'},SHAPER_LIVE);
$interfaceTrafficClass->{'Status'} = CFGM_ONLINE;
return $interfaceTrafficClass->{'TrafficClassID'};
}
# Function to change a traffic class
sub changeInterfaceTrafficClass
{
my $interfaceTrafficClassData = shift;
# Check interface exists first
my $interfaceID;
if (!defined($interfaceID = isInterfaceIDValid($interfaceTrafficClassData->{'InterfaceID'}))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process interface class change as there is no 'InterfaceID' attribute");
return;
}
# Check if traffic class ID is valid
my $trafficClassID;
if (!defined($trafficClassID = isNumber($interfaceTrafficClassData->{'TrafficClassID'},ISNUMBER_ALLOW_ZERO))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process class change as there is no 'TrafficClassID' attribute");
return;
}
if ($trafficClassID && !isTrafficClassIDValid($interfaceTrafficClassData->{'TrafficClassID'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process class change as 'TrafficClassID' attribute is invalid");
return;
}
my $interfaceTrafficClass = $globals->{'Interfaces'}->{$interfaceID}->{'TrafficClasses'}->{$trafficClassID};
my $changes = getHashChanges($interfaceTrafficClass,$interfaceTrafficClassData,[CLASS_CHANGE_ATTRIBUTES]);
# Bump up changes
$globals->{'StateChanged'}++;
# Flag changed
$interfaceTrafficClass->{'Status'} = CFGM_CHANGED;
# XXX - hack our override in
$interfaceTrafficClass->{'.applied_overrides'}->{'change'} = $changes;
# Add to change queue
$globals->{'InterfaceTrafficClassChangeQueue'}->{$interfaceTrafficClass->{'ID'}} = $interfaceTrafficClass;
# Return what was changed
return dclone($changes);
}
# Function to return a class with any items changed as per class overrides
sub getEffectiveInterfaceTrafficClass2
{
my $interfaceTrafficClassID = shift;
my $interfaceTrafficClass;
if (!defined($interfaceTrafficClass = getInterfaceTrafficClass2($interfaceTrafficClassID))) {
return;
}
my $realInterfaceTrafficClass = $globals->{'InterfaceTrafficClasses'}->{$interfaceTrafficClassID};
# If we have applied class overrides, check out what changes there may be
if (defined(my $appliedClassOverrides = $realInterfaceTrafficClass->{'.applied_overrides'})) {
my $interfaceTrafficClassOverrideSet;
# Loop with class overrides in ascending fashion, least matches to most
foreach my $interfaceTrafficClassID (
sort { $appliedClassOverrides->{$a} <=> $appliedClassOverrides->{$b} } keys %{$appliedClassOverrides}
) {
my $interfaceTrafficClassOverride = $appliedClassOverrides->{$interfaceTrafficClassID};
# Loop with attributes and create our override set
foreach my $attr (CLASS_OVERRIDE_CHANGESET_ATTRIBUTES) {
# Set class override set attribute if the class override has defined it
if (defined($interfaceTrafficClassOverride->{$attr}) && $interfaceTrafficClassOverride->{$attr} ne "") {
$interfaceTrafficClassOverrideSet->{$attr} = $interfaceTrafficClassOverride->{$attr};
}
}
}
# Set class overrides on pool
if (defined($interfaceTrafficClassOverrideSet)) {
foreach my $attr (keys %{$interfaceTrafficClassOverrideSet}) {
$interfaceTrafficClass->{$attr} = $interfaceTrafficClassOverrideSet->{$attr};
}
}
}
return $interfaceTrafficClass;
}
# Function to set interface traffic class shaper state
sub setInterfaceTrafficClassShaperState
{
my ($interfaceTrafficClassID,$state) = @_;
# Check interface traffic class exists first
if (!isInterfaceTrafficClassIDValid2($interfaceTrafficClassID)) {
return;
}
$globals->{'InterfacesTrafficClasses'}->{$interfaceTrafficClassID}->{'.shaper_state'} |= $state;
return $globals->{'InterfacesTrafficClasses'}->{$interfaceTrafficClassID}->{'.shaper_state'};
}
# Function to unset interface traffic class shaper state
sub unsetInterfaceTrafficClassShaperState
{
my ($interfaceTrafficClassID,$state) = @_;
# Check interface traffic class exists first
if (!isInterfaceTrafficClassIDValid2($interfaceTrafficClassID)) {
return;
}
$globals->{'InterfacesTrafficClasses'}->{$interfaceTrafficClassID}->{'.shaper_state'} &= ~$state;
return $globals->{'InterfacesTrafficClasses'}->{$interfaceTrafficClassID}->{'.shaper_state'};
}
# Function to get shaper state for a interface traffic class
sub getInterfaceTrafficClassShaperState
{
my $interfaceTrafficClassID = shift;
# Check interface traffic class exists first
if (!isInterfaceTrafficClassIDValid2($interfaceTrafficClassID)) {
return;
}
return $globals->{'InterfacesTrafficClasses'}->{$interfaceTrafficClassID}->{'.shaper_state'};
}
# Function to get all traffic classes
sub getAllTrafficClasses
{
return ( keys %{$globals->{'TrafficClasses'}} );
}
# Function to get a traffic class
sub getTrafficClass
{
my $trafficClassID = shift;
if (!isTrafficClassIDValid($trafficClassID)) {
return;
}
return $globals->{'TrafficClasses'}->{$trafficClassID};
}
# Function to check if traffic class is valid
sub isTrafficClassIDValid
{
my $trafficClassID = shift;
if (!defined($trafficClassID) || !defined($globals->{'TrafficClasses'}->{$trafficClassID})) {
return;
}
return $trafficClassID;
}
# Function to return the traffic priority based on a traffic class
sub getTrafficClassPriority
{
my $trafficClassID = shift;
# Check it exists first
if (!isTrafficClassIDValid($trafficClassID)) {
return;
}
# NK: Short circuit, our TrafficClassID = Priority
return $trafficClassID;
}
# Function to create an interface
sub createInterface
{
my $interfaceData = shift;
my $interface;
# Check if ID is valid
if (!defined($interface->{'ID'} = $interfaceData->{'ID'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to add interface as ID is invalid");
return;
}
# Check if Interface is valid
if (!defined($interface->{'Device'} = $interfaceData->{'Device'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to add interface as Device is invalid");
return;
}
# Check if Name is valid
if (!defined($interface->{'Name'} = $interfaceData->{'Name'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to add interface as Name is invalid");
return;
}
# Check Limit is valid
if (!defined($interface->{'Limit'} = isNumber($interfaceData->{'Limit'}))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to add interface as Limit is invalid");
return;
}
# Add interface
$globals->{'Interfaces'}->{$interface->{'ID'}} = $interface;
# Create interface main traffic class
createInterfaceTrafficClass({
'InterfaceID' => $interface->{'ID'},
'TrafficClassID' => 0,
'CIR' => $interfaceData->{'Limit'},
'Limit' => $interfaceData->{'Limit'},
});
return $interface->{'ID'};
}
# Function to return if an interface ID is valid
sub isInterfaceIDValid
{
my $interfaceID = shift;
# Return undef if interface is not valid
if (!defined($globals->{'Interfaces'}->{$interfaceID})) {
return;
}
return $interfaceID;
}
# Function to return the configured Interfaces
sub getInterfaces
{
return ( keys %{$globals->{'Interfaces'}} );
}
# Return interface classes
sub getInterface
{
my $interfaceID = shift;
# Check if interface ID is valid
if (!isInterfaceIDValid($interfaceID)) {
return;
}
my $res = dclone($globals->{'Interfaces'}->{$interfaceID});
# We don't want to return TrafficClasses
delete($res->{'TrafficClasses'});
# And return it...
return $res;
}
# Function to return our default pool configuration
sub getInterfaceDefaultPool
{
my $interface = shift;
# We don't really need the interface to return the default pool
return $globals->{'DefaultPool'};
}
# Function to create an interface group
sub createInterfaceGroup
{
my $interfaceGroupData = shift;
my $interfaceGroup;
# Check if Name is valid
if (!defined($interfaceGroup->{'Name'} = $interfaceGroupData->{'Name'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to add interface group as Name is invalid");
return;
}
# Check if TxInterface is valid
if (!defined($interfaceGroup->{'TxInterface'} = isInterfaceIDValid($interfaceGroupData->{'TxInterface'}))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to add interface group as TxInterface is invalid");
return;
}
# Check if RxInterface is valid
if (!defined($interfaceGroup->{'RxInterface'} = isInterfaceIDValid($interfaceGroupData->{'RxInterface'}))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to add interface group as RxInterface is invalid");
return;
}
$interfaceGroup->{'ID'} = sprintf('%s,%s',$interfaceGroup->{'TxInterface'},$interfaceGroup->{'RxInterface'});
$interfaceGroup->{'IPMap'} = { };
# Add interface group
$globals->{'InterfaceGroups'}->{$interfaceGroup->{'ID'}} = $interfaceGroup;
return $interfaceGroup->{'ID'};
}
# Function to get interface groups
sub getInterfaceGroups
{
return ( keys %{$globals->{'InterfaceGroups'}} );
}
# Function to get an interface group
sub getInterfaceGroup
{
my $interfaceGroupID = shift;
if (!isInterfaceGroupIDValid($interfaceGroupID)) {
return;
}
my $interfaceGroup = dclone($globals->{'InterfaceGroups'}->{$interfaceGroupID});
delete($interfaceGroup->{'IPMap'});
return $interfaceGroup;
}
# Function to check if interface group is valid
sub isInterfaceGroupIDValid
{
my $interfaceGroupID = shift;
if (!defined($interfaceGroupID) || !defined($globals->{'InterfaceGroups'}->{$interfaceGroupID})) {
return;
}
return $interfaceGroupID;
}
# Function to get match priorities
sub getMatchPriorities
{
return dclone($config->{'match_priorities'});
}
# Function to check if interface group is valid
sub isMatchPriorityIDValid
{
my $mpid = shift;
# Check all is ok
if (!defined($mpid) || !defined($config->{'match_priorities'}->{$mpid})) {
return;
}
return $mpid;
}
# Function to set a pool attribute
sub setPoolAttribute
{
my ($pid,$attr,$value) = @_;
# Return if it doesn't exist
if (!isPoolIDValid($pid)) {
return;
}
$globals->{'Pools'}->{$pid}->{'.attributes'}->{$attr} = $value;
return $value;
}
# Function to get a pool attribute
sub getPoolAttribute
{
my ($pid,$attr) = @_;
# Return if it doesn't exist
if (!isPoolIDValid($pid)) {
return;
}
# Check if attribute exists first
if (
!defined($globals->{'Pools'}->{$pid}->{'.attributes'}) ||
!defined($globals->{'Pools'}->{$pid}->{'.attributes'}->{$attr}))
{
return;
}
return $globals->{'Pools'}->{$pid}->{'.attributes'}->{$attr};
}
# Function to remove a pool attribute
sub removePoolAttribute
{
my ($pid,$attr) = @_;
# Return if it doesn't exist
if (!isPoolIDValid($pid)) {
return;
}
# Check if attribute exists first
if (
!defined($globals->{'Pools'}->{$pid}->{'.attributes'}) ||
!defined($globals->{'Pools'}->{$pid}->{'.attributes'}->{$attr}))
{
return;
}
return delete($globals->{'Pools'}->{$pid}->{'.attributes'}->{$attr});
}
# Function to return a pool override
sub getPoolOverride
{
my $poid = shift;
if (!isPoolOverrideIDValid($poid)) {
return;
}
my $poolOverride = dclone($globals->{'PoolOverrides'}->{$poid});
return $poolOverride;
}
## Function to return a list of pool override ID's
sub getPoolOverrides
{
return (keys %{$globals->{'PoolOverrides'}});
}
# Function to create a pool
sub createPool
{
my $poolData = shift;
# Check if we have all the attributes we need
my $isInvalid;
foreach my $attr (POOL_REQUIRED_ATTRIBUTES) {
if (!defined($poolData->{$attr})) {
$isInvalid = $attr;
last;
}
}
if ($isInvalid) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool add for '%s' as there is an attribute missing: '%s'",
prettyUndef($poolData->{'Name'}),
$isInvalid
);
return;
}
my $pool;
my $now = time();
# Now check if the name is valid
if (!defined($pool->{'Name'} = $poolData->{'Name'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool add as Name is invalid");
return;
}
# Check interface group ID is OK
if (!defined($pool->{'InterfaceGroupID'} = isInterfaceGroupIDValid($poolData->{'InterfaceGroupID'}))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool add for '%s' as the InterfaceGroupID is invalid",
$pool->{'Name'}
);
return;
}
# If we already have this name added, return it as the pool
if (defined(my $pool = $globals->{'PoolNameMap'}->{$pool->{'InterfaceGroupID'}}->{$pool->{'Name'}})) {
return $pool->{'ID'};
}
# Check class is OK
if (!defined($pool->{'TrafficClassID'} = isTrafficClassIDValid($poolData->{'TrafficClassID'}))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool add for '%s' as the TrafficClassID is invalid",
$pool->{'Name'}
);
return;
}
# Make sure things are not attached to the default pool
if (defined($globals->{'DefaultPool'}) && $pool->{'TrafficClassID'} eq $globals->{'DefaultPool'}) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool add for '%s' as the TrafficClassID is the default pool class",
$pool->{'Name'}
);
return;
}
# Check traffic limits
if (!isNumber($pool->{'TxCIR'} = $poolData->{'TxCIR'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool add for '%s' as the TxCIR is invalid",
$pool->{'Name'}
);
return;
}
if (!isNumber($pool->{'RxCIR'} = $poolData->{'RxCIR'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool add for '%s' as the RxCIR is invalid",
$pool->{'Name'}
);
return;
}
# If we don't have burst limits, improvize
if (!defined($pool->{'TxLimit'} = $poolData->{'TxLimit'})) {
$pool->{'TxLimit'} = $pool->{'TxCIR'};
$pool->{'TxCIR'} = int($pool->{'TxLimit'}/4);
}
if (!defined($pool->{'RxLimit'} = $poolData->{'RxLimit'})) {
$pool->{'RxLimit'} = $pool->{'RxCIR'};
$pool->{'RxCIR'} = int($pool->{'RxLimit'}/4);
}
# Set source
$pool->{'Source'} = $poolData->{'Source'};
# Set when this entry was created
$pool->{'Created'} = defined($poolData->{'Created'}) ? $poolData->{'Created'} : $now;
$pool->{'LastUpdate'} = $now;
# Set when this entry expires
$pool->{'Expires'} = defined($poolData->{'Expires'}) ? int($poolData->{'Expires'}) : 0;
# Check status is OK
$pool->{'Status'} = CFGM_NEW;
# Set friendly name and notes
$pool->{'FriendlyName'} = $poolData->{'FriendlyName'};
# Set notes
$pool->{'Notes'} = $poolData->{'Notes'};
# Assign pool ID
$pool->{'ID'} = $globals->{'PoolIDCounter'}++;
# NK: Need better pool ID determination, check what ID is available
# Add pool
$globals->{'Pools'}->{$pool->{'ID'}} = $pool;
# Link pool name map
$globals->{'PoolNameMap'}->{$pool->{'InterfaceGroupID'}}->{$pool->{'Name'}} = $pool;
# Blank our pool member mapping
$globals->{'PoolMemberMap'}->{$pool->{'ID'}} = { };
setPoolShaperState($pool->{'ID'},SHAPER_NOTLIVE);
# Pool needs updating
$globals->{'PoolChangeQueue'}->{$pool->{'ID'}} = $pool;
# Resolve pool overrides
_resolve_pool_override([$pool->{'ID'}]);
# Bump up changes
$globals->{'StateChanged'}++;
return $pool->{'ID'};
}
# Function to remove a pool
sub removePool
{
my $pid = shift;
# Check pool exists first
if (!isPoolIDValid($pid)) {
return;
}
my $pool = $globals->{'Pools'}->{$pid};
# Check if pool is not already offlining
if ($pool->{'Status'} == CFGM_OFFLINE) {
return;
}
my $now = time();
# Set status to offline so its caught by our garbage collector
$pool->{'Status'} = CFGM_OFFLINE;
# Updated pool's last updated timestamp
$pool->{'LastUpdate'} = $now;
# Pool needs updating
$globals->{'PoolChangeQueue'}->{$pool->{'ID'}} = $pool;
# Bump up changes
$globals->{'StateChanged'}++;
return;
}
# Function to change a pool
sub changePool
{
my $poolData = shift;
# Check pool exists first
if (!isPoolIDValid($poolData->{'ID'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool change as there is no 'ID' attribute");
return;
}
my $pool = $globals->{'Pools'}->{$poolData->{'ID'}};
my $now = time();
my $changes = getHashChanges($pool,$poolData,[POOL_CHANGE_ATTRIBUTES]);
# Make changes...
foreach my $item (keys %{$changes}) {
$pool->{$item} = $changes->{$item};
}
# Set pool to being updated
$pool->{'Status'} = CFGM_CHANGED;
# Pool was just updated, so update our timestamp
$pool->{'LastUpdate'} = $now;
# Pool needs updating
$globals->{'PoolChangeQueue'}->{$pool->{'ID'}} = $pool;
# Bump up changes
$globals->{'StateChanged'}++;
# Return what was changed
return dclone($changes);
}
# Function to return a pool
sub getPool
{
my $pid = shift;
if (!isPoolIDValid($pid)) {
return;
}
my $pool = dclone($globals->{'Pools'}->{$pid});
# Remove attributes?
delete($pool->{'.attributes'});
delete($pool->{'.applied_overrides'});
return $pool;
}
# Function to get a pool by its name
sub getPoolByName
{
my ($interfaceGroupID,$name) = @_;
# Make sure both params are defined or we get warnings
if (!defined($interfaceGroupID) || !defined($name)) {
return;
}
# Maybe it doesn't exist?
if (
!defined($globals->{'PoolNameMap'}->{$interfaceGroupID}) ||
!defined($globals->{'PoolNameMap'}->{$interfaceGroupID}->{$name}))
{
return;
}
return dclone($globals->{'PoolNameMap'}->{$interfaceGroupID}->{$name});
}
# Function to return a list of pool ID's
sub getPools
{
return (keys %{$globals->{'Pools'}});
}
# Function to return a pool TX interface
sub getPoolTxInterface
{
my $pid = shift;
# Check pool exists first
if (!isPoolIDValid($pid)) {
return;
}
return $globals->{'InterfaceGroups'}->{$globals->{'Pools'}->{$pid}->{'InterfaceGroupID'}}->{'TxInterface'};
}
# Function to return a pool RX interface
sub getPoolRxInterface
{
my $pid = shift;
# Check pool exists first
if (!isPoolIDValid($pid)) {
return;
}
return $globals->{'InterfaceGroups'}->{$globals->{'Pools'}->{$pid}->{'InterfaceGroupID'}}->{'RxInterface'};
}
# Function to return a pool traffic class ID
sub getPoolTrafficClassID
{
my $pid = shift;
# Check pool exists first
if (!isPoolIDValid($pid)) {
return;
}
return $globals->{'Pools'}->{$pid}->{'TrafficClassID'};
}
# Function to set pools shaper state
sub setPoolShaperState
{
my ($pid,$state) = @_;
# Check pool exists first
if (!isPoolIDValid($pid)) {
return;
}
$globals->{'Pools'}->{$pid}->{'.shaper_state'} |= $state;
return $globals->{'Pools'}->{$pid}->{'.shaper_state'};
}
# Function to unset pools shaper state
sub unsetPoolShaperState
{
my ($pid,$state) = @_;
# Check pool exists first
if (!isPoolIDValid($pid)) {
return;
}
$globals->{'Pools'}->{$pid}->{'.shaper_state'} &= ~$state;
return $globals->{'Pools'}->{$pid}->{'.shaper_state'};
}
# Function to get shaper state for a pool
sub getPoolShaperState
{
my $pid = shift;
# Check pool exists first
if (!isPoolIDValid($pid)) {
return;
}
return $globals->{'Pools'}->{$pid}->{'.shaper_state'};
}
# Function to check the pool ID exists
sub isPoolIDValid
{
my $pid = shift;
if (!defined($pid) || !defined($globals->{'Pools'}->{$pid})) {
return;
}
return $pid;
}
# Function to return if a pool is ready for any kind of modification
sub isPoolReady
{
my $pid = shift;
# Get state and check pool exists all in one
my $state;
if (!defined($state = getPoolShaperState($pid))) {
return;
}
return ($globals->{'Pools'}->{$pid}->{'Status'} == CFGM_ONLINE && $state & SHAPER_LIVE);
}
# Function to check if pool is being overridden or not
sub isPoolOverridden
{
my $pid = shift;
if (!isPoolIDValid($pid)) {
return;
}
# Set a property based on if this pool is overridden or not
if (defined($globals->{'Pools'}->{$pid}->{'.applied_overrides'}) &&
(keys %{$globals->{'Pools'}->{$pid}->{'.applied_overrides'}}) > 0) {
return 1;
}
return 0;
}
# Function to return a pool with any items changed as per pool overrides
sub getEffectivePool
{
my $pid = shift;
my $pool;
if (!defined($pool = getPool($pid))) {
return;
}
my $realPool = $globals->{'Pools'}->{$pid};
# If we have applied pool overrides, check out what changes there may be
if (defined(my $appliedPoolOverrides = $realPool->{'.applied_overrides'})) {
my $poolOverrideSet;
# Loop with pool overrides in ascending fashion, least matches to most
foreach my $poid ( sort { $appliedPoolOverrides->{$a} <=> $appliedPoolOverrides->{$b} } keys %{$appliedPoolOverrides}) {
my $poolOverride = $globals->{'PoolOverrides'}->{$poid};
# Loop with attributes and create our pool override set
foreach my $attr (POOL_OVERRIDE_CHANGESET_ATTRIBUTES) {
# Set pool override set attribute if the pool override has defined it
if (defined($poolOverride->{$attr}) && $poolOverride->{$attr} ne "") {
$poolOverrideSet->{$attr} = $poolOverride->{$attr};
}
}
}
# Set pool overrides on pool
if (defined($poolOverrideSet)) {
foreach my $attr (keys %{$poolOverrideSet}) {
$pool->{$attr} = $poolOverrideSet->{$attr};
}
}
}
return $pool;
}
# Function to create a pool member
sub createPoolMember
{
my $poolMemberData = shift;
# Check if we have all the attributes we need
my $isInvalid;
foreach my $attr (POOLMEMBER_REQUIRED_ATTRIBUTES) {
if (!defined($poolMemberData->{$attr})) {
$isInvalid = $attr;
last;
}
}
if ($isInvalid) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool member add as there is an attribute missing: '%s'",$isInvalid);
return;
}
my $poolMember;
my $now = time();
# Check if IP address is defined
if (!defined(isIPv46CIDR($poolMember->{'IPAddress'} = $poolMemberData->{'IPAddress'}))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool member add as the IPAddress is invalid");
return;
}
if (defined($poolMemberData->{'IPNATAddress'}) && $poolMemberData->{'IPNATAddress'} ne "") {
if (!defined(isIPv46($poolMember->{'IPNATAddress'} = $poolMemberData->{'IPNATAddress'}))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool member add as the IPNATAddress is invalid");
return;
} elsif (defined($poolMemberData->{'IPNATInbound'}) && $poolMemberData->{'IPNATInbound'} eq "yes") {
$poolMember->{'IPNATInbound'} = "yes";
}
}
# Now check if Username its valid
if (!defined(isUsername($poolMember->{'Username'} = $poolMemberData->{'Username'}, ISUSERNAME_ALLOW_ATSIGN))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool member add as Username is invalid");
return;
}
# Check pool ID is OK
if (!defined($poolMember->{'PoolID'} = isPoolIDValid($poolMemberData->{'PoolID'}))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool member add for '%s' as the PoolID is invalid",
$poolMemberData->{'Username'}
);
return;
}
# Grab pool
my $pool = $globals->{'Pools'}->{$poolMember->{'PoolID'}};
# Check match priority ID is OK
if (!defined($poolMember->{'MatchPriorityID'} = isMatchPriorityIDValid($poolMemberData->{'MatchPriorityID'}))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool member add for '%s' as the MatchPriorityID is invalid",
$poolMemberData->{'Username'}
);
return;
}
# Check group ID is OK
if (!defined($poolMember->{'GroupID'} = isGroupIDValid($poolMemberData->{'GroupID'}))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool member add for '%s' as the GroupID is invalid",
$poolMemberData->{'Username'}
);
return;
}
# Set source
$poolMember->{'Source'} = $poolMemberData->{'Source'};
# Set when this entry was created
$poolMember->{'Created'} = defined($poolMemberData->{'Created'}) ? $poolMemberData->{'Created'} : $now;
$poolMember->{'LastUpdate'} = $now;
# Set when this entry expires
$poolMember->{'Expires'} = defined($poolMemberData->{'Expires'}) ? int($poolMemberData->{'Expires'}) : 0;
# Check status is OK
$poolMember->{'Status'} = CFGM_NEW;
# Set friendly name and notes
$poolMember->{'FriendlyName'} = $poolMemberData->{'FriendlyName'};
# Set notes
$poolMember->{'Notes'} = $poolMemberData->{'Notes'};
# Create pool member ID
$poolMember->{'ID'} = $globals->{'PoolMemberIDCounter'}++;
# Add pool member
$globals->{'PoolMembers'}->{$poolMember->{'ID'}} = $poolMember;
# Link pool map
$globals->{'PoolMemberMap'}->{$pool->{'ID'}}->{$poolMember->{'ID'}} = $poolMember;
# Updated pool's last updated timestamp
$pool->{'LastUpdate'} = $now;
# Make sure pool is online and not offlining
if ($pool->{'Status'} == CFGM_OFFLINE) {
$pool->{'Status'} = CFGM_ONLINE;
}
setPoolMemberShaperState($poolMember->{'ID'},SHAPER_NOTLIVE);
# Check for IP conflicts
if (
defined($globals->{'InterfaceGroups'}->{$pool->{'InterfaceGroupID'}}->{'IPMap'}->{$poolMember->{'IPAddress'}}) &&
(my @conflicts = keys %{$globals->{'InterfaceGroups'}->{$pool->{'InterfaceGroupID'}}->{'IPMap'}
->{$poolMember->{'IPAddress'}}}) > 0
) {
# Loop wiht conflicts and build some log items to use
my @logItems;
foreach my $pmid (@conflicts) {
my $cPoolMember = $globals->{'PoolMembers'}->{$pmid};
my $cPool = $globals->{'Pools'}->{$cPoolMember->{'PoolID'}};
push(@logItems,sprintf("Pool:%s/Member:%s",$cPool->{'Name'},$cPoolMember->{'Username'}));
}
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Pool '%s' member '%s' IP '%s' conflicts with: %s",
$pool->{'Name'},
$poolMember->{'Username'},
$poolMember->{'IPAddress'},
join(", ",@logItems)
);
# We don't have to add it to the queue, as its in a conflicted state
setPoolMemberShaperState($poolMember->{'ID'},SHAPER_CONFLICT);
} else {
# Pool member needs updating
$globals->{'PoolMemberChangeQueue'}->{$poolMember->{'ID'}} = $poolMember;
}
# Link interface IP address map, we must do the check above FIRST, as that needs the pool to be added to the pool map
$globals->{'InterfaceGroups'}->{$pool->{'InterfaceGroupID'}}->{'IPMap'}->{$poolMember->{'IPAddress'}}
->{$poolMember->{'ID'}} = $poolMember;
# Resolve pool overrides, there may of been no pool members, now there is one and we may be able to apply a pool override
_resolve_pool_override([$pool->{'ID'}]);
# Bump up changes
$globals->{'StateChanged'}++;
return $poolMember->{'ID'};
}
# Function to remove pool member, this function is actually just going to flag it offline
# the offline pool members will be caught by cleanup and removed, we do this because we
# need the pool member setup in the removal functions, we cannot remove it first, and we
# cannot allow plugins to remove internal data structures either.
sub removePoolMember
{
my $pmid = shift;
# Check pool member exists first
if (!isPoolMemberIDValid($pmid)) {
return;
}
my $poolMember = $globals->{'PoolMembers'}->{$pmid};
# Check if pool member is not already offlining
if ($poolMember->{'Status'} == CFGM_OFFLINE) {
return;
}
my $now = time();
# Grab pool
my $pool = $globals->{'Pools'}->{$poolMember->{'PoolID'}};
# Updated pool's last updated timestamp
$pool->{'LastUpdate'} = $now;
# Set status to offline so its caught by our garbage collector
$poolMember->{'Status'} = CFGM_OFFLINE;
# Update pool members last updated timestamp
$poolMember->{'LastUpdate'} = $now;
# Pool member needs updating
$globals->{'PoolMemberChangeQueue'}->{$poolMember->{'ID'}} = $poolMember;
# Bump up changes
$globals->{'StateChanged'}++;
return;
}
# Function to change a pool member
sub changePoolMember
{
my $poolMemberData = shift;
# Check pool member exists first
if (!isPoolMemberIDValid($poolMemberData->{'ID'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool change as there is no 'ID' attribute");
return;
}
my $poolMember = $globals->{'PoolMembers'}->{$poolMemberData->{'ID'}};
my $pool = $globals->{'Pools'}->{$poolMember->{'PoolID'}};
my $now = time();
my $changes = getHashChanges($poolMember,$poolMemberData,[POOLMEMBER_CHANGE_ATTRIBUTES]);
# Make changes...
foreach my $item (keys %{$changes}) {
$poolMember->{$item} = $changes->{$item};
}
# Pool member isn't really updated, so we just set the last updated timestamp
$poolMember->{'LastUpdate'} = $now;
# Pool was just updated, so update our timestamp
$pool->{'LastUpdate'} = $now;
# Bump up changes
$globals->{'StateChanged'}++;
# Return what was changed
return dclone($changes);
}
# Function to return a list of pool ID's
sub getPoolMembers
{
my $pid = shift;
# Check pool exists first
if (!isPoolIDValid($pid)) {
return;
}
# Check our member map is not undefined
if (!defined($globals->{'PoolMemberMap'}->{$pid})) {
return;
}
return keys %{$globals->{'PoolMemberMap'}->{$pid}};
}
# Function to return a pool member
sub getPoolMember
{
my $pmid = shift;
# Check pool member exists first
if (!isPoolMemberIDValid($pmid)) {
return;
}
my $poolMember = dclone($globals->{'PoolMembers'}->{$pmid});
# Remove attributes?
delete($poolMember->{'.attributes'});
return $poolMember;
}
# Function to return a list of pool ID's
sub getPoolMemberByUsernameIP
{
my ($pid,$username,$ipAddress) = @_;
# Check pool exists first
if (!isPoolIDValid($pid)) {
return;
}
# Check our member map is not undefined
if (!defined($globals->{'PoolMemberMap'}->{$pid})) {
return;
}
# Loop with pool members and grab the match, there can only be one as we cannot conflict username and IP
foreach my $pmid (keys %{$globals->{'PoolMemberMap'}->{$pid}}) {
my $poolMember = $globals->{'PoolMemberMap'}->{$pid}->{$pmid};
if ($poolMember->{'Username'} eq $username && $poolMember->{'IPAddress'} eq $ipAddress) {
return $pmid;
}
}
return;
}
# Function to return pool member ID's with a certain IP address using an interface group
sub getAllPoolMembersByInterfaceGroupIP
{
my ($interfaceGroupID,$ipAddress) = @_;
# Make sure both params are defined or we get warnings
if (!defined($interfaceGroupID) || !defined($ipAddress)) {
return;
}
# Maybe it doesn't exist?
if (!defined($globals->{'InterfaceGroups'}->{$interfaceGroupID}->{'IPMap'}->{$ipAddress})) {
return;
}
return keys %{$globals->{'InterfaceGroups'}->{$interfaceGroupID}->{'IPMap'}->{$ipAddress}};
}
# Function to check the pool member ID exists
sub isPoolMemberIDValid
{
my $pmid = shift;
if (!defined($pmid) || !defined($globals->{'PoolMembers'}->{$pmid})) {
return;
}
return $pmid;
}
# Function to return if a pool member is ready for any kind of modification
sub isPoolMemberReady
{
my $pmid = shift;
# Check pool exists first
if (!isPoolMemberIDValid($pmid)) {
return;
}
return ($globals->{'PoolMembers'}->{$pmid}->{'Status'} == CFGM_ONLINE && getPoolMemberShaperState($pmid) & SHAPER_LIVE);
}
# Function to return a pool member match priority
sub getPoolMemberMatchPriority
{
my $pmid = shift;
# Check pool member exists first
if (!isPoolMemberIDValid($pmid)) {
return;
}
# NK: No actual mappping yet, we just return the ID
return $globals->{'PoolMembers'}->{$pmid}->{'MatchPriorityID'};
}
# Function to set a pool member attribute
sub setPoolMemberAttribute
{
my ($pmid,$attr,$value) = @_;
# Check pool member exists first
if (!isPoolMemberIDValid($pmid)) {
return;
}
$globals->{'PoolMembers'}->{$pmid}->{'.attributes'}->{$attr} = $value;
return $value;
}
# Function to set pool member shaper state
sub setPoolMemberShaperState
{
my ($pmid,$state) = @_;
# Check pool member exists first
if (!isPoolMemberIDValid($pmid)) {
return;
}
$globals->{'PoolMembers'}->{$pmid}->{'.shaper_state'} |= $state;
return $globals->{'PoolMembers'}->{$pmid}->{'.shaper_state'};
}
# Function to unset pool member shaper state
sub unsetPoolMemberShaperState
{
my ($pmid,$state) = @_;
# Check pool member exists first
if (!isPoolMemberIDValid($pmid)) {
return;
}
$globals->{'PoolMembers'}->{$pmid}->{'.shaper_state'} &= ~$state;
return $globals->{'PoolMembers'}->{$pmid}->{'.shaper_state'};
}
# Function to get shaper state for a pool
sub getPoolMemberShaperState
{
my $pmid = shift;
# Check pool member exists first
if (!isPoolMemberIDValid($pmid)) {
return;
}
return $globals->{'PoolMembers'}->{$pmid}->{'.shaper_state'};
}
# Function to get a pool member attribute
sub getPoolMemberAttribute
{
my ($pmid,$attr) = @_;
# Read event for server
sub process_change {
my ($kernel, $user) = @_[KERNEL, ARG0];
# Check pool member exists first
if (!isPoolMemberIDValid($pmid)) {
return;
}
# Check if attribute exists first
if (
!defined($globals->{'PoolMembers'}->{$pmid}->{'.attributes'}) ||
!defined($globals->{'PoolMembers'}->{$pmid}->{'.attributes'}->{$attr}))
{
return;
}
return $globals->{'PoolMembers'}->{$pmid}->{'.attributes'}->{$attr};
}
# Function to remove a pool member attribute
sub removePoolMemberAttribute
{
my ($pmid,$attr) = @_;
# Check pool member exists first
if (!isPoolMemberIDValid($pmid)) {
return;
}
# Check if attribute exists first
if (
!defined($globals->{'PoolMembers'}->{$pmid}->{'.attributes'}) ||
!defined($globals->{'PoolMembers'}->{$pmid}->{'.attributes'}->{$attr}))
{
return;
}
return delete($globals->{'PoolMembers'}->{$pmid}->{'.attributes'}->{$attr});
}
# Create a limit, which is the combination of a pool and a pool member
sub createLimit
{
my $limitData = shift;
# Check if we have all the attributes we need
my $isInvalid;
foreach my $attr (LIMIT_REQUIRED_ATTRIBUTES) {
if (!defined($limitData->{$attr})) {
$isInvalid = $attr;
last;
}
}
if ($isInvalid) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process limit add as there is an attribute missing: '%s'",$isInvalid);
return;
}
# Check if IP address is defined
if (!defined(isIPv46CIDR($limitData->{'IPAddress'} = $limitData->{'IPAddress'}))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process limit add as the IPAddress is invalid");
return;
}
if (defined($limitData->{'IPNATAddress'}) && $limitData->{'IPNATAddress'} ne "") {
if (!defined(isIPv46($limitData->{'IPNATAddress'} = $limitData->{'IPNATAddress'}))) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process limit add as the IPNATAddress is invalid");
return;
}
}
if (defined($limitData->{'IPNATInbound'}) && $limitData->{'IPNATInbound'} eq "yes") {
$limitData->{'IPNATInbound'} = "yes";
}
my $poolName = $limitData->{'Username'};
my $poolData = {
'FriendlyName' => $limitData->{'FriendlyName'} || $limitData->{'IPAddress'},
'Name' => $poolName,
'InterfaceGroupID' => $limitData->{'InterfaceGroupID'},
'TrafficClassID' => $limitData->{'TrafficClassID'},
'TxCIR' => $limitData->{'TxCIR'},
'TxLimit' => $limitData->{'TxLimit'},
'RxCIR' => $limitData->{'RxCIR'},
'RxLimit' => $limitData->{'RxLimit'},
'Expires' => $limitData->{'Expires'},
'Notes' => $limitData->{'Notes'},
'Source' => $limitData->{'Source'}
};
# If we didn't succeed just exit
my $poolID;
if (!defined($poolID = createPool($poolData))) {
return;
}
my $poolMemberData = {
'FriendlyName' => $limitData->{'FriendlyName'},
'Username' => $limitData->{'Username'},
'IPAddress' => $limitData->{'IPAddress'},
'InterfaceGroupID' => $limitData->{'InterfaceGroupID'},
'MatchPriorityID' => $limitData->{'MatchPriorityID'},
'PoolID' => $poolID,
'GroupID' => $limitData->{'GroupID'},
'Expires' => $limitData->{'Expires'},
'Notes' => $limitData->{'Notes'},
'Source' => $limitData->{'Source'}
};
my $poolMemberID;
if (!defined($poolMemberID = createPoolMember($poolMemberData))) {
return;
}
return ($poolMemberID,$poolID);
}
# Function to create a pool override
sub createPoolOverride
{
my $poolOverrideData = shift;
# Check that we have at least one match attribute
my $isValid = 0;
foreach my $item (POOL_OVERRIDE_MATCH_ATTRIBUTES) {
# Bump up $isValid if we have a match attribute
if (defined($poolOverrideData->{$item})) {
$isValid++;
}
}
if (!$isValid) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool override as there is no selection attribute");
return;
}
my $poolOverride;
my $now = time();
# Pull in attributes
foreach my $item (POOL_OVERRIDE_ATTRIBUTES) {
$poolOverride->{$item} = $poolOverrideData->{$item};
}
# Check group is OK
if (defined($poolOverride->{'GroupID'}) && !isGroupIDValid($poolOverride->{'GroupID'})) {
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Cannot process pool override for user '%s', IP '%s', GroupID '%s' as the ".
"GroupID is invalid",
prettyUndef($poolOverride->{'Username'}),
prettyUndef($poolOverride->{'IPAddress'}),
prettyUndef($poolOverride->{'GroupID'})
);
return;
}
# Check class is OK
if (defined($poolOverride->{'TrafficClassID'}) && !isTrafficClassIDValid($poolOverride->{'TrafficClassID'})) {
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Cannot process pool override for user '%s', IP '%s', GroupID '%s' as the ".
"TrafficClassID is invalid",
prettyUndef($poolOverride->{'Username'}),
prettyUndef($poolOverride->{'IPAddress'}),
prettyUndef($poolOverride->{'GroupID'})
);
return;
}
# Set source
$poolOverride->{'Source'} = $poolOverrideData->{'Source'};
# Set when this entry was created
$poolOverride->{'Created'} = defined($poolOverrideData->{'Created'}) ? $poolOverrideData->{'Created'} : $now;
$poolOverride->{'LastUpdate'} = $now;
# Set when this entry expires
$poolOverride->{'Expires'} = defined($poolOverrideData->{'Expires'}) ? int($poolOverrideData->{'Expires'}) : 0;
# Check status is OK
$poolOverride->{'Status'} = CFGM_NEW;
# Set friendly name and notes
$poolOverride->{'FriendlyName'} = $poolOverrideData->{'FriendlyName'};
# Set notes
$poolOverride->{'Notes'} = $poolOverrideData->{'Notes'};
# Create pool member ID
$poolOverride->{'ID'} = $globals->{'PoolOverrideIDCounter'}++;
# Add pool override
$globals->{'PoolOverrides'}->{$poolOverride->{'ID'}} = $poolOverride;
# Resolve pool overrides
_resolve_pool_override(undef,[$poolOverride->{'ID'}]);
# Bump up changes
$globals->{'StateChanged'}++;
return $poolOverride->{'ID'};
}
# Function to remove a pool override
sub removePoolOverride
{
my $poid = shift;
# Check pool override exists first
if (!isPoolOverrideIDValid($poid)) {
return;
}
my $poolOverride = $globals->{'PoolOverrides'}->{$poid};
# Remove pool override from pools that have it and trigger a change
if (defined($poolOverride->{'.applied_pools'})) {
foreach my $pid (keys %{$poolOverride->{'.applied_pools'}}) {
my $pool = $globals->{'Pools'}->{$pid};
# Remove pool overrides from the pool
delete($pool->{'.applied_overrides'}->{$poolOverride->{'ID'}});
# If the pool is online and live, trigger a change
if ($pool->{'Status'} == CFGM_ONLINE && getPoolShaperState($pid) & SHAPER_LIVE) {
$globals->{'PoolChangeQueue'}->{$pool->{'ID'}} = $pool;
$pool->{'Status'} = CFGM_CHANGED;
}
}
}
# Remove pool override
delete($globals->{'PoolOverrides'}->{$poolOverride->{'ID'}});
# Bump up changes
$globals->{'StateChanged'}++;
return;
}
# Function to change a pool override
sub changePoolOverride
{
my $poolOverrideData = shift;
# Check pool override exists first
if (!isPoolOverrideIDValid($poolOverrideData->{'ID'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process pool override change as there is no 'ID' attribute");
return;
}
my $poolOverride = $globals->{'PoolOverrides'}->{$poolOverrideData->{'ID'}};
my $now = time();
my $changes = getHashChanges($poolOverride,$poolOverrideData,[POOL_OVERRIDE_CHANGE_ATTRIBUTES]);
# Make changes...
foreach my $item (keys %{$changes}) {
$poolOverride->{$item} = $changes->{$item};
}
# Set status to updated
$poolOverride->{'Status'} = CFGM_CHANGED;
# Set timestamp
$poolOverride->{'LastUpdate'} = $now;
# Resolve pool overrides to see if any attributes changed, we only do this if it already matches
# We do NOT support changing match attributes
if (defined($poolOverride->{'.applied_pools'}) && (my @pids = keys %{$poolOverride->{'.applied_pools'}}) > 0) {
_resolve_pool_override([@pids],[$poolOverride->{'ID'}]);
}
# Bump up changes
$globals->{'StateChanged'}++;
# Return what was changed
return dclone($changes);
}
# Function to check the pool override ID exists
sub isPoolOverrideIDValid
{
my $poid = shift;
if (!defined($poid) || !defined($globals->{'PoolOverrides'}->{$poid})) {
return;
}
return $poid;
}
#
# Internal functions
#
# Resolve all pool overrides or those linked to a pid or oid
# We take 2 optional argument, which is a single pool override and a single pool to process
sub _resolve_pool_override
{
my ($pids,$poids) = @_;
# Hack to intercept and create a single element hash if we get ID's above
my $poolHash;
if (defined($pids)) {
foreach my $pid (@{$pids}) {
$poolHash->{$pid} = $globals->{'Pools'}->{$pid};
}
} else {
$poolHash = $globals->{'Pools'};
}
my $poolOverrideHash;
if (defined($poids)) {
foreach my $poid (@{$poids}) {
$poolOverrideHash->{$poid} = $globals->{'PoolOverrides'}->{$poid};
}
} else {
$poolOverrideHash = $globals->{'PoolOverrides'};
}
# Loop with all pools, keep a list of pid's updated
my $matchList;
while ((my $pid, my $pool) = each(%{$poolHash})) {
# Build a candidate from the pool
my $candidate = {
'PoolName' => $pool->{'Name'},
};
# If we only have 1 member in the pool, add its username, IP and group
if ((my ($pmid) = getPoolMembers($pid)) == 1) {
my $poolMember = getPoolMember($pmid);
$candidate->{'Username'} = $poolMember->{'Username'};
$candidate->{'IPAddress'} = $poolMember->{'IPAddress'};
$candidate->{'GroupID'} = $poolMember->{'GroupID'};
}
# Loop with all pool overrides and generate a match list
while ((my $poid, my $poolOverride) = each(%{$poolOverrideHash})) {
my $numMatches = 0;
my $numMismatches = 0;
# Loop with the attributes and check for a full match
foreach my $attr (POOL_OVERRIDE_MATCH_ATTRIBUTES) {
# If this attribute in the pool override is set, then lets check it
if (defined($poolOverride->{$attr}) && $poolOverride->{$attr} ne "") {
# Check for match or mismatch, only if candidate attribute is defined
if (defined($candidate->{$attr})) {
if ($candidate->{$attr} eq $poolOverride->{$attr}) {
$numMatches++;
} else {
$numMismatches++;
}
}
}
}
# Setup the match list with what was matched
if ($numMatches && !$numMismatches) {
$matchList->{$pid}->{$poid} = $numMatches;
} else {
$matchList->{$pid}->{$poid} = undef;
}
}
}
# Loop with the match list
foreach my $pid (keys %{$matchList}) {
my $pool = $globals->{'Pools'}->{$pid};
# Original Effective pool
my $oePool = getEffectivePool($pid);
# Loop with pool overrides for this pool
foreach my $poid (keys %{$matchList->{$pid}}) {
my $poolOverride = $globals->{'PoolOverrides'}->{$poid};
# If we have a match, record it in pools & pool overrides
if (defined($matchList->{$pid}->{$poid})) {
# Setup trakcing of what is applied to what
$globals->{'PoolOverrides'}->{$poid}->{'.applied_pools'}->{$pid} = $matchList->{$pid}->{$poid};
$globals->{'Pools'}->{$pid}->{'.applied_overrides'}->{$poid} = $matchList->{$pid}->{$poid};
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Pool override '%s' [%s] applied to pool '%s' [%s]",
$poolOverride->{'FriendlyName'},
$poolOverride->{'ID'},
$pool->{'Name'},
$pool->{'ID'}
);
# We didn't match, but we may of matched before?
} else {
# There was a pool override before, so something changed now that there is none
if (defined($globals->{'Pools'}->{$pid}->{'.applied_overrides'}->{$poid})) {
# Remove pool overrides
delete($globals->{'Pools'}->{$pid}->{'.applied_overrides'}->{$poid});
delete($globals->{'PoolOverrides'}->{$poid}->{'.applied_pools'}->{$pid});
$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Pool override '%s' no longer applies to pool '%s' [%s]",
$poolOverride->{'ID'},
$pool->{'Name'},
$pool->{'ID'}
);
}
}
}
# New Effective pool
my $nePool = getEffectivePool($pid);
# Get changes between effective pool states
my $poolChanges = getHashChanges($oePool,$nePool,[POOL_OVERRIDE_CHANGESET_ATTRIBUTES]);
# If there were pool changes, trigger a pool update
if (keys %{$poolChanges} > 0) {
# If the pool is currently online and live, trigger a change
if ($pool->{'Status'} == CFGM_ONLINE && getPoolShaperState($pid) & SHAPER_LIVE) {
$pool->{'Status'} = CFGM_CHANGED;
$globals->{'PoolChangeQueue'}->{$pool->{'ID'}} = $pool;
}
}
}
}
# Remove pool override information
sub _remove_pool_override
{
my $pid = shift;
if (!isPoolIDValid($pid)) {
return;
}
my $pool = $globals->{'Pools'}->{$pid};
# Remove pool from pool overrides if there are any
if (defined($pool->{'.applied_overrides'})) {
foreach my $poid (keys %{$pool->{'.applied_overrides'}}) {
delete($globals->{'PoolOverrides'}->{$poid}->{'.applied_pools'}->{$pool->{'ID'}});
}
}
}
# Load our statefile
sub _load_statefile
{
# Check if the state file exists first of all
if (! -f $config->{'statefile'}) {
$logger->log(LOG_ERR,"[CONFIGMANAGER] Statefile '%s' doesn't exist",$config->{'statefile'});
return;
}
if (! -s $config->{'statefile'}) {
$logger->log(LOG_ERR,"[CONFIGMANAGER] Statefile '%s' has zero size ignoring",$config->{'statefile'});
return;
}
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Loading statefile '%s'",$config->{'statefile'});
# Pull in a hash for our statefile
my %stateHash;
if (! tie %stateHash, 'Config::IniFiles', ( -file => $config->{'statefile'} )) {
# Check if we got errors, if we did use them for our reason
my @errors = @Config::IniFiles::errors;
my $reason = $1 || join('; ',@errors);
$logger->log(LOG_ERR,"[CONFIGMANAGER] Failed to open statefile '%s': %s",$config->{'statefile'},$reason);
# Set it to undef so we don't overwrite it...
if (@errors) {
$config->{'statefile'} = undef;
}
return;
}
# Grab the object handle
my $state = tied( %stateHash );
# Loop with interface traffic class overrides
foreach my $section ($state->GroupMembers('interface_traffic_class.override')) {
my $classOverride = $stateHash{$section};
# Loop with the persistent attributes and create our hash
my $cClassOverride;
foreach my $attr (CLASS_OVERRIDE_PERSISTENT_ATTRIBUTES) {
if (defined($classOverride->{$attr})) {
# If its an array, join all the items
if (ref($classOverride->{$attr}) eq "ARRAY") {
$classOverride->{$attr} = join("\n",@{$classOverride->{$attr}});
}
$cClassOverride->{$attr} = $classOverride->{$attr};
}
}
# XXX - Hack, Proces this class override
changeInterfaceTrafficClass($cClassOverride);
}
# Loop with user pool overrides
foreach my $section ($state->GroupMembers('pool.override')) {
my $poolOverride = $stateHash{$section};
# Loop with the persistent attributes and create our hash
my $cPoolOverride;
foreach my $attr (POOL_OVERRIDE_PERSISTENT_ATTRIBUTES) {
if (defined($poolOverride->{$attr})) {
# If its an array, join all the items
if (ref($poolOverride->{$attr}) eq "ARRAY") {
$poolOverride->{$attr} = join("\n",@{$poolOverride->{$attr}});
}
$cPoolOverride->{$attr} = $poolOverride->{$attr};
}
}
# Proces this pool override
createPoolOverride($cPoolOverride);
}
# We need a pool ID translation, when we recreate pools we get different ID's, we cannot restore members with orignal ID's
my %pidMap;
# Loop with pools
foreach my $section ($state->GroupMembers('pool')) {
my $pool = $stateHash{$section};
# Loop with the attributes to create the hash
my $cpool;
foreach my $attr (POOL_PERSISTENT_ATTRIBUTES) {
if (defined($pool->{$attr})) {
# If its an array, join all the items
if (ref($pool->{$attr}) eq "ARRAY") {
$pool->{$attr} = join("\n",@{$pool->{$attr}});
}
$cpool->{$attr} = $pool->{$attr};
}
}
# Process this pool
if (defined(my $pid = createPool($cpool))) {
# Save the new ID
$pidMap{$pool->{'ID'}} = $pid;
} else {
$logger->log(LOG_ERR,"[CONFIGMANAGER] Failed to load pool '%s' [%s], members will be ignored",
prettyUndef($cpool->{'Name'}),
$section
);
}
}
# Loop with pool members
foreach my $section ($state->GroupMembers('pool_member')) {
my $poolMember = $stateHash{$section};
# Loop with the attributes to create the hash
my $cpoolMember;
foreach my $attr (POOLMEMBER_PERSISTENT_ATTRIBUTES) {
if (defined($poolMember->{$attr})) {
# If its an array, join all the items
if (ref($poolMember->{$attr}) eq "ARRAY") {
$poolMember->{$attr} = join("\n",@{$poolMember->{$attr}});
}
$cpoolMember->{$attr} = $poolMember->{$attr};
}
}
# Translate pool ID
if (my $pid = $pidMap{$cpoolMember->{'PoolID'}}) {
$cpoolMember->{'PoolID'} = $pid;
# Process this pool member
if (!defined(my $pmid = createPoolMember($cpoolMember))) {
$logger->log(LOG_ERR,"[CONFIGMANAGER] Failed to load pool member '%s'",$pmid);
}
} else {
$logger->log(LOG_ERR,"[CONFIGMANAGER] Failed to load pool member '%s', no pool ID map for '%s'",
$cpoolMember->{'Username'},
$cpoolMember->{'PoolID'}
);
}
}
}
# Write out statefile
sub _write_statefile
{
my $fullWrite = shift;
# We reset this early so we don't get triggred continuously if we encounter errors
$globals->{'StateChanged'} = 0;
$globals->{'LastStateSync'} = time();
# Check if the state file exists first of all
if (!defined($config->{'statefile'})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] No statefile defined. Possible initial load error?");
return;
}
# Only write out if we actually have limits & pool overrides, else we may of crashed?
if (!(keys %{$globals->{'Pools'}}) && !(keys %{$globals->{'PoolOverrides'}})) {
$logger->log(LOG_WARN,"[CONFIGMANAGER] Not writing state file as there are no active pools or pool overrides");
return;
}
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] Saving statefile '%s'",$config->{'statefile'});
# Create a unique user identifier
my $userUniq = $user->{'Username'} . "/" . $user->{'IP'};
my $timer1 = [gettimeofday];
# 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;
# Create new state file object
my $state = new Config::IniFiles();
# XXX - Hack, loop with class overrides
while ((my $itcid, my $interfaceTrafficClass) = each(%{$globals->{'InterfaceTrafficClasses'}})) {
# Skip over non-overridden classes
if (!defined($interfaceTrafficClass->{'.applied_overrides'})) {
next;
}
# Create a section name
my $section = "interface_traffic_class.override " . $itcid;
# Add a section for this class override
$state->AddSection($section);
# XXX - Hack, Attributes we want to save for this traffic class override
foreach my $attr (CLASS_OVERRIDE_PERSISTENT_ATTRIBUTES) {
# Set items up
if (defined(my $value = $interfaceTrafficClass->{$attr})) {
$state->newval($section,$attr,$value);
}
}
# XXX - Hack, loop with the override
foreach my $attr (CLASS_OVERRIDE_PERSISTENT_ATTRIBUTES) {
# Set items up
if (defined(my $value = $interfaceTrafficClass->{'.applied_overrides'}->{'change'}->{$attr})) {
$state->newval($section,$attr,$value);
}
}
}
# Loop with pool overrides
while ((my $poid, my $poolOverride) = each(%{$globals->{'PoolOverrides'}})) {
# Create a section name
my $section = "pool.override " . $poid;
# Add a section for this pool override
$state->AddSection($section);
# Attributes we want to save for this pool override
foreach my $attr (POOL_OVERRIDE_PERSISTENT_ATTRIBUTES) {
# Set items up
if (defined(my $value = $globals->{'PoolOverrides'}->{$poid}->{$attr})) {
$state->newval($section,$attr,$value);
}
}
}
# Loop with pools
while ((my $pid, my $pool) = each(%{$globals->{'Pools'}})) {
# Skip over dynamic entries, we only want persistent ones unless we doing a full write
next if (!$fullWrite && $pool->{'Source'} eq "plugin.radius");
# Create a section name
my $section = "pool " . $pid;
# Add a section for this pool
$state->AddSection($section);
# Persistent attributes we want
foreach my $attr (POOL_PERSISTENT_ATTRIBUTES) {
# Set items up
if (defined(my $value = $pool->{$attr})) {
$state->newval($section,$attr,$value);
}
}
# Save pool members too
foreach my $pmid (keys %{$globals->{'PoolMemberMap'}->{$pid}}) {
# Create a section name for the pool member
$section = "pool_member " . $pmid;
# Add a new section for this pool member
$state->AddSection($section);
my $poolMember = $globals->{'PoolMembers'}->{$pmid};
# Items we want for persistent entries
foreach my $attr (POOLMEMBER_PERSISTENT_ATTRIBUTES) {
# Set items up
if (defined(my $value = $poolMember->{$attr})) {
$state->newval($section,$attr,$value);
}
}
}
}
# Check for an error
my $newFilename = $config->{'statefile'}.".new";
if (!defined($state->WriteConfig($newFilename))) {
$logger->log(LOG_ERR,"[CONFIGMANAGER] Failed to write temporary statefile '%s': %s",$newFilename,$!);
return;
}
# If we have a state file, we going to rename it
my $bakFilename = $config->{'statefile'}.".bak";
if (-f $config->{'statefile'}) {
# Check if we could rename/move
if (!rename($config->{'statefile'},$bakFilename)) {
$logger->log(LOG_ERR,"[CONFIGMANAGER] Failed to rename '%s' to '%s': %s",$config->{'statefile'},$bakFilename,$!);
return;
}
}
# Move the new filename in place
if (!rename($newFilename,$config->{'statefile'})) {
$logger->log(LOG_ERR,"[CONFIGMANAGER] Failed to rename '%s' to '%s': %s",$newFilename,$config->{'statefile'},$!);
return;
}
# Set the user ID before we post to the change queue
$user->{'ID'} = $uid;
$user->{'LastUpdate'} = time();
my $timer2 = [gettimeofday];
my $timediff2 = tv_interval($timer1,$timer2);
# Push change to change queue
$changeQueue->{$uid} = $user;
$logger->log(LOG_NOTICE,"[CONFIGMANAGER] State file '%s' saved in %s",$config->{'statefile'},sprintf('%.3fs',$timediff2));
}
......
package Radius::Dictionary;
package opentrafficshaper::plugins::radius::Radius::Dictionary;
use strict;
use warnings;
......
package Radius::Packet;
package opentrafficshaper::plugins::radius::Radius::Packet;
use strict;
require Exporter;
......@@ -13,7 +13,7 @@ $VSA = 26; # Type assigned in RFC2138 to the
# Vendor-Specific Attributes
# Be sure our dictionaries are current
use Radius::Dictionary 1.50;
use opentrafficshaper::plugins::radius::Radius::Dictionary 1.50;
use Carp;
use Socket;
use Digest::MD5;
......
# OpenTrafficShaper radius module
# Copyright (C) 2007-2013, AllWorldIT
#
# Copyright (C) 2007-2023, 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
......@@ -22,11 +22,33 @@ use strict;
use warnings;
use opentrafficshaper::plugins::radius::Radius::Dictionary;
use opentrafficshaper::plugins::radius::Radius::Packet;
use POE;
use IO::Socket::INET;
use opentrafficshaper::logger;
use awitpt::util;
use opentrafficshaper::plugins::configmanager qw(
createPool
changePool
createPoolMember
changePoolMember
createLimit
getPoolByName
getPoolMember
getPoolMembers
getPoolMemberByUsernameIP
isInterfaceGroupIDValid
isTrafficClassIDValid
isMatchPriorityIDValid
isGroupIDValid
);
# Exporter stuff
......@@ -39,8 +61,18 @@ our (@ISA,@EXPORT,@EXPORT_OK);
);
use constant {
VERSION => '0.0.1',
VERSION => '1.0.0',
DATAGRAM_MAXLEN => 8192,
DEFAULT_EXPIRY_PERIOD => 86400,
# Expirty period for removal of entries
REMOVE_EXPIRY_PERIOD => 60,
# IANA public enterprise number
# This is used as the radius vendor code
IANA_PEN => 42109,
};
......@@ -48,73 +80,223 @@ use constant {
our $pluginInfo = {
Name => "Radius",
Version => VERSION,
Init => \&init,
Init => \&plugin_init,
Start => \&plugin_start,
};
# Copy of system globals
# Our globals
my $globals;
# Copy of system logger
my $logger;
# Our own data storage
my $config = {
'expiry_period' => DEFAULT_EXPIRY_PERIOD,
'username_to_pool_transform' => undef,
'interface_group' => 'eth1,eth0',
'match_priority' => 2,
'traffic_class' => 2,
'group' => 1,
};
# Initialize plugin
sub init
sub plugin_init
{
$globals = shift;
my $system = shift;
# Setup our environment
$logger = $globals->{'logger'};
$logger = $system->{'logger'};
$logger->log(LOG_NOTICE,"[RADIUS] OpenTrafficShaper Radius Module v%s - Copyright (c) 2013-2014, AllWorldIT",VERSION);
# Inititalize
$globals->{'Dictionary'} = undef;
# Split off dictionaries to load
my @dicts = ref($system->{'file.config'}->{'plugin.radius'}->{'dictionary'}) eq "ARRAY" ?
@{$system->{'file.config'}->{'plugin.radius'}->{'dictionary'}} :
( $system->{'file.config'}->{'plugin.radius'}->{'dictionary'} );
foreach my $dict (@dicts) {
$dict =~ s/\s+//g;
# Skip comments
next if ($dict =~ /^#/);
# Check if we have a path, if we do use it
if (defined($system->{'file.config'}->{'plugin.radius'}->{'dictionary_path'})) {
$dict = $system->{'file.config'}->{'plugin.radius'}->{'dictionary_path'} . "/$dict";
}
push(@{$config->{'config.dictionaries'}},$dict);
}
# Load dictionaries
$logger->log(LOG_DEBUG,"[RADIUS] Loading dictionaries...");
my $dict = new opentrafficshaper::plugins::radius::Radius::Dictionary;
foreach my $df (@{$config->{'config.dictionaries'}}) {
# Load dictionary
if ($dict->readfile($df)) {
$logger->log(LOG_INFO,"[RADIUS] Loaded dictionary '%s'",$df);
} else {
$logger->log(LOG_WARN,"[RADIUS] Failed to load dictionary '%s': %s",$df,$!);
}
}
$logger->log(LOG_DEBUG,"[RADIUS] Loading dictionaries completed.");
# Store the dictionary
$globals->{'Dictionary'} = $dict;
# Check if we must override the expiry time
if (defined(my $expiry = $system->{'file.config'}->{'plugin.radius'}->{'expiry_period'})) {
$logger->log(LOG_INFO,"[RADIUS] Set expiry_period to '%s'",$expiry);
$config->{'expiry_period'} = $expiry;
}
# Check if we got a username to pool transform
if (defined(my $userPoolTransform = $system->{'file.config'}->{'plugin.radius'}->{'username_to_pool_transform'})) {
$logger->log(LOG_INFO,"[RADIUS] Set username_to_pool_transform to '%s'",$userPoolTransform);
$config->{'username_to_pool_transform'} = $userPoolTransform;
}
# Default interface group to use
if (defined(my $interfaceGroup = $system->{'file.config'}->{'plugin.radius'}->{'default_interface_group'})) {
if (isInterfaceGroupIDValid($interfaceGroup)) {
$logger->log(LOG_INFO,"[RADIUS] Set interface_group to '%s'",$interfaceGroup);
$config->{'interface_group'} = $interfaceGroup;
} else {
$logger->log(LOG_WARN,"[RADIUS] Cannot set 'interface_group' as value '%s' is invalid",$interfaceGroup);
}
} else {
$logger->log(LOG_INFO,"[RADIUS] Using default interface_group '%s'",$config->{'interface_group'});
}
# Default match priority to use
if (defined(my $matchPriority = $system->{'file.config'}->{'plugin.radius'}->{'default_match_priority'})) {
if (isMatchPriorityIDValid($matchPriority)) {
$logger->log(LOG_INFO,"[RADIUS] Set match_priority to '%s'",$matchPriority);
$config->{'match_priority'} = $matchPriority;
} else {
$logger->log(LOG_WARN,"[RADIUS] Cannot set 'match_priority' as value '%s' is invalid",$matchPriority);
}
}
# Default traffic class to use
if (defined(my $trafficClassID = $system->{'file.config'}->{'plugin.radius'}->{'default_traffic_class'})) {
if (isTrafficClassIDValid($trafficClassID)) {
$logger->log(LOG_INFO,"[RADIUS] Set traffic_class to '%s'",$trafficClassID);
$config->{'traffic_class'} = $trafficClassID;
} else {
$logger->log(LOG_WARN,"[RADIUS] Cannot set 'traffic_class' as value '%s' is invalid",$trafficClassID);
}
}
# Default group to use
if (defined(my $group = $system->{'file.config'}->{'plugin.radius'}->{'default_group'})) {
if (isGroupIDValid($group)) {
$logger->log(LOG_INFO,"[RADIUS] Set group to '%s'",$group);
$config->{'group'} = $group;
} else {
$logger->log(LOG_WARN,"[RADIUS] Cannot set 'group' as value '%s' is invalid",$group);
}
}
# Radius listener
POE::Session->create(
inline_states => {
_start => \&server_init,
get_datagram => \&server_read,
_start => \&_session_start,
_stop => \&_session_stop,
_socket_read => \&_session_socket_read,
}
);
$logger->log(LOG_NOTICE,"[RADIUS] OpenTrafficShaper Radius Module v".VERSION." - Copyright (c) 2013, AllWorldIT")
return 1;
}
# Initialize server
sub server_init {
my $kernel = $_[KERNEL];
my $socket = IO::Socket::INET->new(
Proto => 'udp',
LocalPort => '1813',
);
die "Couldn't create server socket: $!" unless $socket;
# Start the plugin
sub plugin_start
{
$logger->log(LOG_INFO,"[RADIUS] Started");
}
# Initialize server
sub _session_start
{
my ($kernel,$heap) = @_[KERNEL,HEAP];
# Create socket for radius
if (!defined($heap->{'socket'} = IO::Socket::INET->new(
Proto => 'udp',
# TODO - Add to config file
# LocalAddr => '192.168.254.2',
LocalPort => '1813',
))) {
$logger->log(LOG_ERR,"Failed to create Radius listening socket: %s",$!);
return;
}
# Set our alias
$kernel->alias_set("plugin.radius");
# Setup our reader
$kernel->select_read($socket, "get_datagram");
# Setup our socket reader event
$kernel->select_read($heap->{'socket'}, "_socket_read");
$logger->log(LOG_DEBUG,"[RADIUS] Initialized");
}
# Shut down server
sub _session_stop
{
my ($kernel,$heap) = @_[KERNEL,HEAP];
# Tear down the socket select
if (defined($heap->{'socket'})) {
$kernel->select_read($heap->{'socket'},undef);
}
# Blow everything away
$globals = undef;
$logger->log(LOG_DEBUG,"[RADIUS] Shutdown");
$logger = undef;
}
# Read event for server
sub server_read {
sub _session_socket_read
{
my ($kernel, $socket) = @_[KERNEL, ARG0];
# Read in packet from the socket
my $peer = recv($socket, my $udp_packet = "", DATAGRAM_MAXLEN, 0);
# If we don't have a peer, just return
return unless defined $peer;
if (!defined($peer)) {
$logger->log(LOG_WARN,"[RADIUS] Peer appears to be undefined");
return;
}
# Get peer port and addy from remote host
my ($peer_port, $peer_addr) = unpack_sockaddr_in($peer);
my $peer_addr_h = inet_ntoa($peer_addr);
# Parse packet
my $pkt = new Radius::Packet($globals->{'radius'}->{'dictionary'},$udp_packet);
my $pkt = opentrafficshaper::plugins::radius::Radius::Packet->new($globals->{'Dictionary'},$udp_packet);
# Build log line
my $logLine = sprintf("Remote: $peer_addr_h, Code: %s, Identifier: %s => ",$pkt->code,$pkt->identifier);
my $logLine = sprintf("Remote: %s:%s, Code: %s, Identifier: %s => ",$peer_addr_h,$peer_port,$pkt->code,$pkt->identifier);
foreach my $attr ($pkt->attributes) {
$logLine .= sprintf(" %s: '%s',", $attr, $pkt->rawattr($attr));
}
......@@ -137,71 +319,317 @@ sub server_read {
$logLine .= sprintf(" %s/%s: %s,",$vendor,$attr,$attrVal);
}
}
$logger->log(LOG_DEBUG,"[RADIUS] $logLine");
$logger->log(LOG_DEBUG,"[RADIUS] %s",$logLine);
# TODO - verify packet
# Time now
my $now = time();
# Pull in a variables from packet
my $username = $pkt->rawattr("User-Name");
my $trafficGroup;
if (my $attrRawVal = $pkt->vsattr(11111,'OpenTrafficShaper-Traffic-Group')) {
$trafficGroup = @{ $attrRawVal }[0];
my $group = $config->{'group'};
if (my $attrRawVal = $pkt->vsattr(IANA_PEN,'OpenTrafficShaper-Traffic-Group')) {
my $var = @{ $attrRawVal }[0];
# Next check if its valid
if (isGroupIDValid($var)) {
$group = $var;
} else {
$logger->log(LOG_WARN,"[RADIUS] Cannot set 'group' for user '%s' as value '%s' is invalid, using default '%s'",
$username,
$var,
$group
);
}
}
my $trafficClass;
if (my $attrRawVal = $pkt->vsattr(11111,'OpenTrafficShaper-Traffic-Class')) {
$trafficClass = @{ $attrRawVal }[0];
my $trafficClassID = $config->{'traffic_class'};
if (my $attrRawVal = $pkt->vsattr(IANA_PEN,'OpenTrafficShaper-Traffic-Class')) {
my $var = @{ $attrRawVal }[0];
# Check if its valid
if (isTrafficClassIDValid($var)) {
$trafficClassID = $var;
} else {
$logger->log(LOG_WARN,"[RADIUS] Cannot set 'traffic_class' for user '%s' as value '%s' is invalid, using default '%s'",
$username,
$var,
$trafficClassID
);
}
}
my $trafficLimit;
if (my $attrRawVal = $pkt->vsattr(11111,'OpenTrafficShaper-Traffic-Limit')) {
if (my $attrRawVal = $pkt->vsattr(IANA_PEN,'OpenTrafficShaper-Traffic-Limit')) {
$trafficLimit = @{ $attrRawVal }[0];
}
# We assume below that we will have limits
if (!defined($trafficLimit)) {
$logger->log(LOG_NOTICE,"[RADIUS] No traffic limit set for user '%s', ignoring",$username);
return;
}
# Grab rate limits below from the string we got
my $rxCIR; my $txCIR;
my $rxLimit; my $txLimit;
# Match rx-rate[/tx-rate] rx-burst-rate[/tx-burst-rate]
if ($trafficLimit =~ /^(\d+)([km])(?:\/(\d+)([km]))?(?: (\d+)([km])(?:\/(\d+)([km]))?)?/) {
$rxCIR = getKbit($1,$2);
$txCIR = getKbit($3,$4);
$rxLimit = getKbit($5,$6);
$txLimit = getKbit($7,$8);
# Set our limits if they not defined
if (!defined($rxLimit)) {
$rxLimit = $rxCIR;
$rxCIR = $rxCIR / 4;
}
if (!defined($txLimit)) {
$txLimit = $txCIR;
$txCIR = $txCIR / 4;
}
# Grab rate limits from the string we got
my $trafficLimitRx = 0; my $trafficLimitTx = 0;
my $trafficLimitRxBurst = 0; my $trafficLimitTxBurst = 0;
if (defined($trafficLimit)) {
my ($trafficLimitRxQuantifier,$trafficLimitTxQuantifier);
my ($trafficLimitRxBurstQuantifier,$trafficLimitTxBurstQuantifier);
# Match rx-rate[/tx-rate] rx-burst-rate[/tx-burst-rate]
if ($trafficLimit =~ /^(\d+)([km])(?:\/(\d+)([km]))?(?: (\d+)([km])(?:\/(\d+)([km]))?)?/) {
$trafficLimitRx = getKbit($1,$2);
$trafficLimitTx = getKbit($3,$4);
$trafficLimitRxBurst = getKbit($5,$6);
$trafficLimitTxBurst = getKbit($7,$8);
} else {
$logger->log(LOG_WARN,"[RADIUS] The 'OpenTrafficShaper-Traffic-Limit' attribute appears to be invalid for user '%s'".
": '%s'",
$username,
$trafficLimit
);
return;
}
# Check if we have a pool transform
my $poolName;
if (defined($config->{'username_to_pool_transform'})) {
# Check if transform matches, if it does set pool name
if ($username =~ $config->{'username_to_pool_transform'}) {
$poolName = $1;
}
}
# Set default if they undefined
if (!defined($trafficGroup)) {
$trafficGroup = 0;
# Check if the pool name is being overridden
if (my $attrRawVal = $pkt->vsattr(IANA_PEN,'OpenTrafficShaper-Traffic-Pool')) {
$poolName = @{ $attrRawVal }[0];
}
if (!defined($trafficClass)) {
$trafficClass = 0;
# If we got a pool name, check if it exists
if (defined($poolName)) {
if (!defined(getPoolByName($config->{'interface_group'},$poolName))) {
$logger->log(LOG_NOTICE,"[RADIUS] Pool '%s' not found, using username '%s' instead",
$poolName,
$username
);
$poolName = $username;
}
# If we didn't get the pool name, just use the username
} else {
$poolName = $username;
}
my $user = {
'Username' => $username,
'IP' => $pkt->attr('Framed-IP-Address'),
'Group' => $trafficGroup,
'GroupName' => "Group 1",
'Class' => $trafficClass,
'ClassName' => "Class A",
'Limits' => "$trafficLimitTx / $trafficLimitRx",
'BurstLimits' => "$trafficLimitTxBurst / $trafficLimitRxBurst",
'Status' => getStatus($pkt->rawattr('Acct-Status-Type')),
};
# Try grab the pool
my $pool = getPoolByName($config->{'interface_group'},$poolName);
my $pid = defined($pool) ? $pool->{'ID'} : undef;
my $ipAddress = $pkt->attr('Framed-IP-Address');
my $statusType = getStatus($pkt->rawattr('Acct-Status-Type'));
$logger->log(LOG_INFO,"[RADIUS] Status: %s, User: %s, IP: %s, InterfaceGroup: %s, MatchPriorityID: %s, Group: %s, Class: %s, ".
"CIR: %s/%s, Limit: %s/%s",
$statusType,
$username,
$ipAddress,
$config->{'interface_group'},
$config->{'match_priority'},
$group,
$trafficClassID,
prettyUndef($txCIR),
prettyUndef($rxCIR),
prettyUndef($txLimit),
prettyUndef($rxLimit)
);
# Throw the change at the config manager
$kernel->post("configmanager" => "process_change" => $user);
# Check if user is new or online
if ($statusType eq "new" || $statusType eq "online") {
# Check if pool is defined
if (defined($pool)) {
my @poolMembers = getPoolMembers($pid);
# Check if we created the pool
if ($pool->{'Source'} eq "plugin.radius") {
# Make sure the pool is 0 or 1
if (@poolMembers < 2) {
# Change the details
my $changes = changePool({
'ID' => $pid,
'FriendlyName' => $ipAddress,
'TrafficClassID' => $trafficClassID,
'TxCIR' => $txCIR,
'RxCIR' => $rxCIR,
# These MUST be defined
'TxLimit' => $txLimit,
'RxLimit' => $rxLimit,
'Expires' => $now + DEFAULT_EXPIRY_PERIOD
});
my @txtChanges;
foreach my $item (keys %{$changes}) {
# Make expires look nice
my $value = $changes->{$item};
if ($item eq "Expires") {
$value = sprintf("%s [%s]",$value,scalar(localtime($value)));
}
push(@txtChanges,sprintf("%s = %s",$item,$value));
}
if (@txtChanges) {
$logger->log(LOG_INFO,"[RADIUS] Pool '%s' updated: %s",$poolName,join(", ",@txtChanges));
}
# If we do have more than 1 member, make a note of it
} else {
$logger->log(LOG_NOTICE,"[RADIUS] Pool '%s' has more than 1 member, not updating",$poolName);
}
}
# No pool, time to create one
} else {
# If we don't have rate limits, short circuit
if (!defined($txCIR)) {
$logger->log(LOG_NOTICE,"[RADIUS] Pool '%s' has no 'TxCIR', aborting",$poolName);
return;
}
if (!defined($rxCIR)) {
$logger->log(LOG_NOTICE,"[RADIUS] Pool '%s' has no 'RxCIR', aborting",$poolName);
return;
}
# Create pool
$pid = createPool({
'FriendlyName' => $ipAddress,
'Name' => $poolName,
'InterfaceGroupID' => $config->{'interface_group'},
'TrafficClassID' => $trafficClassID,
'TxCIR' => $txCIR,
'RxCIR' => $rxCIR,
'TxLimit' => $txLimit,
'RxLimit' => $rxLimit,
'Expires' => $now + $config->{'expiry_period'},
'Source' => "plugin.radius",
});
if (!defined($pid)) {
$logger->log(LOG_WARN,"[RADIUS] Pool '%s' failed to create, aborting",$poolName);
return;
}
}
# If we have a pool member
if (defined(my $pmid = getPoolMemberByUsernameIP($pid,$username,$ipAddress))) {
my $poolMember = getPoolMember($pmid);
# Check if we created the pool member
if ($poolMember->{'Source'} eq "plugin.radius") {
my $changes = changePoolMember({
'ID' => $poolMember->{'ID'},
'Expires' => $now + DEFAULT_EXPIRY_PERIOD
});
my @txtChanges;
foreach my $item (keys %{$changes}) {
# Make expires look nice
my $value = $changes->{$item};
if ($item eq "Expires") {
$value = sprintf("%s [%s]",$value,scalar(localtime($value)));
}
push(@txtChanges,sprintf("%s = %s",$item,$value));
}
if (@txtChanges) {
$logger->log(LOG_INFO,"[RADIUS] Pool '%s' member '%s' updated: %s",
$poolName,
$username,
join(", ",@txtChanges)
);
}
# TODO: Add output of updated items here too?
changePool({
'ID' => $pid,
'FriendlyName' => $ipAddress
});
# If not display message
} else {
$logger->log(LOG_NOTICE,"[RADIUS] Pool '%s' member '%s' update ignored as it was not added by 'plugin.radius'",
$poolName,
$username
);
}
# We have a pool but no member...
} else {
createPoolMember({
'FriendlyName' => $username,
'Username' => $username,
'IPAddress' => $ipAddress,
'InterfaceGroupID' => $config->{'interface_group'},
'MatchPriorityID' => $config->{'match_priority'},
'PoolID' => $pid,
'GroupID' => $group,
'Expires' => $now + $config->{'expiry_period'},
'Source' => "plugin.radius",
});
# TODO: Add output of updated items here too?
changePool({
'ID' => $pid,
'FriendlyName' => $ipAddress
});
}
# Radius user going offline
} elsif ($statusType eq "offline") {
# Check if we have a pool
if (defined($pool)) {
# Grab pool members
my @poolMembers = getPoolMembers($pool->{'ID'});
# If this is ours we can set the expires to "queue" removal
if ($pool->{'Source'} eq "plugin.radius") {
# If there is only 1 pool member, then lets expire the pool in the removal expiry period
if (@poolMembers == 1) {
$logger->log(LOG_INFO,"[RADIUS] Expiring pool '$poolName'");
changePool({
'ID' => $pool->{'ID'},
'Expires' => $now + REMOVE_EXPIRY_PERIOD
});
}
}
# Check if we have a pool member with this username and IP
if (my $pmid = getPoolMemberByUsernameIP($pool->{'ID'},$username,$ipAddress)) {
$logger->log(LOG_INFO,"[RADIUS] Expiring pool '$poolName' member '$username'");
changePoolMember({
'ID' => $pmid,
'Expires' => $now + REMOVE_EXPIRY_PERIOD
});
}
$logger->log(LOG_INFO,"[RADIUS] Pool '$poolName' member '$username' set to expire as they're offline");
# No pool
} else {
$logger->log(LOG_DEBUG,"[RADIUS] Pool '$poolName' member '$username' doesn't exist went offline");
}
} else {
$logger->log(LOG_WARN,"[RADIUS] Unknown radius code '%s' for pool '%s' member '%s'",$pkt->code,$poolName,$username);
}
$logger->log(LOG_DEBUG,"=> Code: $user->{'Status'}, User: $user->{'Username'}, IP: $user->{'IP'}, Group: $user->{'Group'}, Class: $user->{'Class'}, Limits: $user->{'Limits'}, Burst: $user->{'BurstLimits'}");
}
# Convert status into something easy to useful
sub getStatus
{
......@@ -220,17 +648,16 @@ sub getStatus
# Simple function to reduce everything to kbit
sub getKbit
{
my ($counter,$quantifier) = @_;
# If there is no counter, return 0
return 0 if (!defined($counter));
# If there is no counter
return if (!defined($counter));
# We need a quantifier
return undef if (!defined($quantifier));
return if (!defined($quantifier));
# Initialize counter
my $newCounter = $counter;
......@@ -240,11 +667,13 @@ sub getKbit
} elsif ($quantifier =~ /^k$/i) {
$newCounter = $counter * 1;
} else {
return undef;
return;
}
return $newCounter;
}
1;
# vim: ts=4
# OpenTrafficShaper Traffic shaping statistics
# Copyright (C) 2007-2015, 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::statistics;
use strict;
use warnings;
use Data::Dumper;
use POE;
use Storable qw( dclone );
use awitpt::db::dblayer;
use opentrafficshaper::constants;
use opentrafficshaper::logger;
use opentrafficshaper::plugins::configmanager qw(
getPool
getPools
getPoolMembers
getPoolTxInterface
getPoolRxInterface
getPoolTrafficClassID
getTrafficClasses
getAllTrafficClasses
);
# NK: TODO: Maybe we want to remove timing at some stage? maybe not?
use Time::HiRes qw( gettimeofday tv_interval );
# Exporter stuff
require Exporter;
our (@ISA,@EXPORT,@EXPORT_OK);
@ISA = qw(Exporter);
@EXPORT = qw(
STATISTICS_PERIOD
STATISTICS_DIR_TX
STATISTICS_DIR_RX
);
@EXPORT_OK = qw(
getLastStats
getStatsBySID
getStatsBasicBySID
getSIDFromCID
getSIDFromPID
);
use constant {
VERSION => '0.2.2',
# How often our config check ticks
TICK_PERIOD => 2,
STATISTICS_PERIOD => 5,
STATISTICS_DIR_TX => 1,
STATISTICS_DIR_RX => 2,
STATISTICS_MAXFLUSH_PER_PERIOD => 10000,
# SQL Statements
SQL_ADD_IDENTIFIER => 'INSERT INTO identifiers (`Identifier`) VALUES (?)',
SQL_GET_IDENTIFIER => 'SELECT ID FROM identifiers WHERE `Identifier` = ?',
SQL_CONSOLIDATE_STATS => '
SELECT
`IdentifierID`, `Timestamp` - (`Timestamp` % ?) AS Timestamp,
`Direction`,
MAX(`CIR`) AS `CIR`, MAX(`Limit`) AS `Limit`, MAX(`Rate`) AS `Rate`, MAX(`PPS`) AS `PPS`,
MAX(`QueueLen`) AS `QueueLen`, MAX(`TotalBytes`) AS `TotalBytes`, MAX(`TotalPackets`) AS `TotalPackets`,
MAX(`TotalOverLimits`) AS `TotalOverLimits`, MAX(`TotalDropped`) AS `TotalDropped`
FROM
stats
WHERE
`Key` = ?
AND `Timestamp` > ?
AND `Timestamp` < ?
GROUP BY
`IdentifierID`, `Timestamp`, `Direction`
',
SQL_CONSOLIDATE_STATS_BASIC => '
SELECT
`IdentifierID`, `Timestamp` - (`Timestamp` % ?) AS Timestamp,
MAX(`Counter`) AS `Counter`
FROM
stats_basic
WHERE
`Key` = ?
AND `Timestamp` > ?
AND `Timestamp` < ?
GROUP BY
`IdentifierID`, `Timestamp`
',
SQL_GET_STATS => '
SELECT
`Timestamp`, `Direction`, `Rate`, `PPS`, `CIR`, `Limit`
FROM
stats
WHERE
`IdentifierID` = ?
AND `Key` = ?
AND `Timestamp` > ?
AND `Timestamp` < ?
',
SQL_GET_STATS_BASIC => '
SELECT
`Timestamp`, `Counter`
FROM
stats_basic
WHERE
`IdentifierID` = ?
AND `Key` = ?
AND `Timestamp` > ?
AND `Timestamp` < ?
',
SQL_CLEANUP_STATS => 'DELETE FROM stats WHERE `Key` = ? AND `Timestamp` < ?',
SQL_CLEANUP_STATS_BASIC => 'DELETE FROM stats_basic WHERE `Key` = ? AND `Timestamp` < ?'
};
sub STATS_CONFIG
{
{
1 => {
'precision' => 300, # 5min
'retention' => 4, # 4 days
},
2 => {
'precision' => 900, # 15min
'retention' => 14, # 14 days
},
3 => {
'precision' => 3600, # 1hr
'retention' => 28 * 2, # 2 months
},
4 => {
'precision' => 21600, # 6hr
'retention' => 28 * 6, # 6 months
},
5 => {
'precision' => 86400, # 24hr
'retention' => 28 * 12 * 2, # 2 years
}
}
}
# Plugin info
our $pluginInfo = {
Name => "Statistics Interface",
Version => VERSION,
Init => \&plugin_init,
Start => \&plugin_start,
};
# Copy of system globals
my $globals;
my $logger;
# Handle of DBI
#
# $globals->{'Database'}->{'Handle'}
# $globals->{'Database'}->{'DSN'}
# $globals->{'Database'}->{'Username'}
# $globals->{'Database'}->{'Password'}
# DB identifier map
#
# $globals->{'IdentifierMap'}
# Stats queue
#
# $globals->{'StatsQueue'}
# $globals->{'LastCleanup'}
# $globals->{'LastConfigManagerStats'}
# Stats subscribers & counter
# $globals->{'SIDSubscribers'}
# $globals->{'SSIDMap'}
# $globals->{'SSIDCounter'}
# $globals->{'SSIDCounterFreeList'}
# Initialize plugin
sub plugin_init
{
$globals = shift;
# Setup our environment
$logger = $globals->{'logger'};
$logger->log(LOG_NOTICE,"[STATISTICS] OpenTrafficShaper Statistics v%s - Copyright (c) 2007-2014, AllWorldIT",VERSION);
# Initialize
$globals->{'Database'} = undef;
$globals->{'IdentifierMap'} = { };
$globals->{'StatsQueue'} = [ ];
$globals->{'LastCleanup'} = { };
$globals->{'LastConfigManagerStats'} = { };
$globals->{'SIDSubscribers'} = { };
$globals->{'SSIDMap'} = { };
$globals->{'SSIDCounter'} = 0;
$globals->{'SSIDCounterFreeList'} = [ ];
# Check our interfaces
if (defined(my $dbdsn = $globals->{'file.config'}->{'plugin.statistics'}->{'db_dsn'})) {
$logger->log(LOG_INFO,"[STATISTICS] Set database DSN to '%s'",$dbdsn);
$globals->{'Database'}->{'DSN'} = $dbdsn;
if (defined(my $dbuser = $globals->{'file.config'}->{'plugin.statistics'}->{'db_username'})) {
$logger->log(LOG_INFO,"[STATISTICS] Set database username to '%s'",$dbuser);
$globals->{'Database'}->{'Username'} = $dbuser;
}
if (defined(my $dbpass = $globals->{'file.config'}->{'plugin.statistics'}->{'db_password'})) {
$logger->log(LOG_INFO,"[STATISTICS] Set database password to '%s'",$dbpass);
$globals->{'Database'}->{'Password'} = $dbpass;
}
} else {
$logger->log(LOG_WARN,"[STATISTICS] No database DSN to specified in configuration file. Stats storage disabled!");
}
# This is our main stats session
POE::Session->create(
inline_states => {
_start => \&_session_start,
_stop => \&_session_stop,
_tick => \&_session_tick,
# Stats update event
update => \&_session_update,
}
);
# Create DBI agent
if (defined($globals->{'Database'})) {
$globals->{'Database'}->{'Handle'} = DBInit($globals->{'Database'});
# Check if handle is defined
if (defined($globals->{'Database'}->{'Handle'})) {
# Try connect (0 is success)
if (!DBConnect()) {
$logger->log(LOG_INFO,"[STATISTICS] Connected to database");
} else {
$logger->log(LOG_ERR,"[STATISTICS] Failed to connect to database: %s (DATABASE DISABLED)",
awitpt::db::dblayer::Error());
# Don't try again
delete($globals->{'Database'});
}
# If the handle is not defined, the database won't work
} else {
$logger->log(LOG_ERR,"[STATISTICS] Failed to initailize database: %s (DATABASE DISABLED)",
awitpt::db::dblayer::Error());
}
# Set last cleanup to now
my $now = time();
foreach my $key (keys %{STATS_CONFIG()}) {
# Get aligned time so we cleanup sooner
$globals->{'LastCleanup'}->{$key} = _getAlignedTime($now,STATS_CONFIG()->{$key}->{'precision'});
}
$globals->{'LastConfigManagerStats'} = $now;
}
return 1;
}
# Start the plugin
sub plugin_start
{
$logger->log(LOG_INFO,"[STATISTICS] Started");
}
# Initialize this plugins main POE session
sub _session_start
{
my ($kernel,$heap) = @_[KERNEL, HEAP];
# Set our alias
$kernel->alias_set("statistics");
# Set delay on config updates
$kernel->delay('_tick' => TICK_PERIOD);
$logger->log(LOG_DEBUG,"[STATISTICS] Initialized");
}
# Stop session
sub _session_stop
{
my ($kernel,$heap) = @_[KERNEL, HEAP];
# Remove our alias
$kernel->alias_remove("statistics");
# Tear down data
$globals = undef;
$logger->log(LOG_DEBUG,"[STATISTICS] Shutdown");
$logger = undef;
}
# Time ticker for processing changes
sub _session_tick
{
my ($kernel,$heap) = @_[KERNEL,HEAP];
# If we don't have a database, just skip...
if (!$globals->{'Database'}) {
return;
}
my $now = time();
my $timer1 = [gettimeofday];
# Even out flushing over 10s to absorb spikes
my $totalFlush = @{$globals->{'StatsQueue'}};
my $maxFlush = int($totalFlush / 10) + 100;
my $numFlush = 0;
# Make sure we don't write more than 10k entries per pass
if ($maxFlush > STATISTICS_MAXFLUSH_PER_PERIOD) {
$maxFlush = STATISTICS_MAXFLUSH_PER_PERIOD;
}
# Loop and build the data to create our multi-insert
my (@insertHolders,@insertBasicHolders);
my (@insertData,@insertBasicData);
while (defined(my $stat = shift(@{$globals->{'StatsQueue'}})) && $numFlush < $maxFlush) {
# This is a basic counter
if (defined($stat->{'Counter'})) {
push(@insertBasicHolders,"(?,?,?,?)");
push(@insertBasicData,
$stat->{'IdentifierID'}, $stat->{'Key'}, $stat->{'Timestamp'},
$stat->{'Counter'}
);
# Full stats counter
} else {
push(@insertHolders,"(?,?,?,?,?,?,?,?,?,?,?,?,?)");
push(@insertData,
$stat->{'IdentifierID'}, $stat->{'Key'}, $stat->{'Timestamp'},
$stat->{'Direction'},
$stat->{'CIR'}, $stat->{'Limit'}, $stat->{'Rate'}, $stat->{'PPS'}, $stat->{'QueueLen'},
$stat->{'TotalBytes'}, $stat->{'TotalPackets'}, $stat->{'TotalOverLimits'}, $stat->{'TotalDropped'}
);
}
$numFlush++;
}
# If we got things to insert, do it
if (@insertBasicHolders > 0) {
my $res = DBDo('
INSERT INTO stats_basic
(
`IdentifierID`, `Key`, `Timestamp`,
`Counter`
)
VALUES
'.join(',',@insertBasicHolders),@insertBasicData
);
# Check for error
if (!defined($res)) {
$logger->log(LOG_ERR,"[STATISTICS] Failed to execute stats_basic insert: %s",awitpt::db::dblayer::Error());
}
}
# And normal stats...
if (@insertHolders > 0) {
my $res = DBDo('
INSERT INTO stats
(
`IdentifierID`, `Key`, `Timestamp`,
`Direction`,
`CIR`, `Limit`, `Rate`, `PPS`, `QueueLen`,
`TotalBytes`, `TotalPackets`, `TotalOverLimits`, `TotalDropped`
)
VALUES
'.join(',',@insertHolders),@insertData
);
# Check for error
if (!defined($res)) {
$logger->log(LOG_ERR,"[STATISTICS] Failed to execute stats insert: %s",awitpt::db::dblayer::Error());
}
}
my $timer2 = [gettimeofday];
# We only need stats if we did something, right?
if ($numFlush) {
my $timediff2 = tv_interval($timer1,$timer2);
$logger->log(LOG_INFO,"[STATISTICS] Total stats flush time %s/%s records: %s",
$numFlush,
$totalFlush,
sprintf('%.3fs',$timediff2)
);
}
my $res;
# Loop with our stats consolidation configuration
foreach my $key (sort keys %{STATS_CONFIG()}) {
my $timerA = [gettimeofday];
my $precision = STATS_CONFIG()->{$key}->{'precision'};
my $thisPeriod = _getAlignedTime($now,$precision);
my $lastPeriod = $thisPeriod - $precision;
my $prevKey = $key - 1;
# If we havn't exited the last period, then skip
if ($globals->{'LastCleanup'}->{$key} > $lastPeriod) {
next;
}
# Stats
my $numStatsBasicConsolidated = 0;
my $numStatsConsolidated = 0;
my $consolidateFrom = $lastPeriod - $precision * 2;
my $consolidateUpTo = $lastPeriod - $precision;
# Execute and pull in consolidated stats
$res = DBSelect(SQL_CONSOLIDATE_STATS_BASIC,$precision,$prevKey,$consolidateFrom,$consolidateUpTo);
if ($res) {
# Loop with items returned
while (my $item = hashifyLCtoMC($res->fetchrow_hashref(),'IdentifierID','Timestamp','Counter')) {
$item->{'Key'} = $key;
# Queue for insert
push(@{$globals->{'StatsQueue'}},$item);
$numStatsBasicConsolidated++;
}
DBFreeRes($res);
# If there was an error, make sure we report it
} else {
$logger->log(LOG_ERR,"[STATISTICS] Failed to execute stats_basic consolidation statement: %s",
awitpt::db::dblayer::Error());
}
# And the normal stats...
$res = DBSelect(SQL_CONSOLIDATE_STATS,$precision,$prevKey,$consolidateFrom,$consolidateUpTo);
if ($res) {
# Loop with items returned
while (my $item = hashifyLCtoMC(
$res->fetchrow_hashref(),
'IdentifierID','Timestamp','Direction','CIR','Limit','Rate','PPS','QueueLen','TotalBytes','TotalPackets',
'TotalOverLimits','TotalDropped'
)) {
$item->{'Key'} = $key;
# Queue for insert
push(@{$globals->{'StatsQueue'}},$item);
$numStatsConsolidated++;
}
DBFreeRes($res);
# If there was an error, make sure we report it
} else {
$logger->log(LOG_ERR,"[STATISTICS] Failed to execute stats consolidation statement: %s",
awitpt::db::dblayer::Error());
}
# Set last cleanup to now
$globals->{'LastCleanup'}->{$key} = $now;
my $timerB = [gettimeofday];
my $timediffB = tv_interval($timerA,$timerB);
$logger->log(LOG_INFO,"[STATISTICS] Stats consolidation: key %s in %s (%s basic, %s normal), period %s - %s [%s - %s]",
$key,
sprintf('%.3fs',$timediffB),
$numStatsBasicConsolidated,
$numStatsConsolidated,
$consolidateFrom,
$consolidateUpTo,
scalar(localtime($consolidateFrom)),
scalar(localtime($consolidateUpTo))
);
}
# Setup another timer
my $timer3 = [gettimeofday];
# We only need to run as often as the first precision
# - If cleanup has not yet run?
# - or if the 0 cleanup plus precision of the first key is in the past (data is now stale?)
if (!defined($globals->{'LastCleanup'}->{'0'}) || $globals->{'LastCleanup'}->{'0'} + STATS_CONFIG()->{1}->{'precision'} < $now) {
# We're going to clean up for the first stats precision * 3, which should be enough
my $cleanUpTo = $now - (STATS_CONFIG()->{1}->{'precision'} * 3);
# Streamed stats is removed 3 time periods past the first precision
my $timerA = [gettimeofday];
if ($res = DBDo(SQL_CLEANUP_STATS_BASIC,0,$cleanUpTo)) {
my $timerB = [gettimeofday];
my $timerdiffA = tv_interval($timerA,$timerB);
# We get 0E0 for 0 when none were removed
if ($res ne "0E0") {
$logger->log(LOG_INFO,"[STATISTICS] Cleanup streamed stats_basic, %s items in %s, up to %s [%s]",
$res,
sprintf('%.3fs',$timerdiffA),
$cleanUpTo,
scalar(localtime($cleanUpTo)),
);
}
} else {
$logger->log(LOG_ERR,"[STATISTICS] Failed to execute stats_basic cleanup statement: %s",
awitpt::db::dblayer::Error());
}
# And the normal stats...
$timerA = [gettimeofday];
if ($res = DBDo(SQL_CLEANUP_STATS,0,$cleanUpTo)) {
my $timerB = [gettimeofday];
my $timerdiffA = tv_interval($timerA,$timerB);
# We get 0E0 for 0 when none were removed
if ($res ne "0E0") {
$logger->log(LOG_INFO,"[STATISTICS] Cleanup streamed stats, %s items in %s, up to %s [%s]",
$res,
sprintf('%.3fs',$timerdiffA),
$cleanUpTo,scalar(localtime($cleanUpTo))
);
}
} else {
$logger->log(LOG_ERR,"[STATISTICS] Failed to execute stats cleanup statement: %s",
awitpt::db::dblayer::Error()
);
}
# Loop and remove retained stats
foreach my $key (keys %{STATS_CONFIG()}) {
# Work out timestamp to clean up to by multiplying the retention period by days
$cleanUpTo = $now - (STATS_CONFIG()->{$key}->{'retention'} * 86400);
# Retention period is in # days
my $timerA = [gettimeofday];
if ($res = DBDo(SQL_CLEANUP_STATS_BASIC,$key,$cleanUpTo)) {
# We get 0E0 for 0 when none were removed
if ($res ne "0E0") {
my $timerB = [gettimeofday];
my $timerdiffA = tv_interval($timerA,$timerB);
$logger->log(LOG_INFO,"[STATISTICS] Cleanup stats_basic key %s in %s, %s items up to %s [%s]",
$key,
sprintf('%.3fs',$timerdiffA),
$res,
$cleanUpTo,
scalar(localtime($cleanUpTo))
);
}
} else {
$logger->log(LOG_ERR,"[STATISTICS] Failed to execute stats_basic cleanup statement for key %s: %s",
$key,
awitpt::db::dblayer::Error()
);
}
# And normal stats...
$timerA = [gettimeofday];
if ($res = DBDo(SQL_CLEANUP_STATS,$key,$cleanUpTo)) {
# We get 0E0 for 0 when none were removed
if ($res ne "0E0") {
my $timerB = [gettimeofday];
my $timerdiffA = tv_interval($timerA,$timerB);
$logger->log(LOG_INFO,"[STATISTICS] Cleanup stats key %s in %s, %s items up to %s [%s]",
$key,
sprintf('%.3fs',$timerdiffA),
$res,
$cleanUpTo,
scalar(localtime($cleanUpTo))
);
}
} else {
$logger->log(LOG_ERR,"[STATISTICS] Failed to execute stats cleanup statement for key %s: %s",
$key,
awitpt::db::dblayer::Error()
);
}
}
# Set last main cleanup to now
$globals->{'LastCleanup'}->{'0'} = $now;
my $timer4 = [gettimeofday];
my $timediff4 = tv_interval($timer3,$timer4);
$logger->log(LOG_INFO,"[STATISTICS] Total stats cleanup time: %s",
sprintf('%.3fs',$timediff4)
);
}
# Check if we need to pull config manager stats
if ($now - $globals->{'LastConfigManagerStats'} > STATISTICS_PERIOD) {
my $configManagerStats = _getConfigManagerStats();
_processStatistics($kernel,$configManagerStats);
$globals->{'LastConfigManagerStats'} = $now;
}
# Set delay on config updates
$kernel->delay('_tick' => TICK_PERIOD);
}
# Update limit Statistics
# $item has some special use cases:
# main:$iface:all - Interface total stats
# main:$iface:classes - Interface classified traffic
# main:$iface:besteffort - Interface best effort traffic
sub _session_update
{
my ($kernel, $statsData) = @_[KERNEL, ARG0];
_processStatistics($kernel,$statsData);
}
# Handle subscriptions to updates
sub subscribe
{
my ($sid,$conversions,$handler,$event) = @_;
$logger->log(LOG_INFO,"[STATISTICS] Got subscription request for '%s': handler='%s', event='%s'",
$sid,
$handler,
$event
);
# Grab next SSID
my $ssid = shift(@{$globals->{'SSIDCounterFreeList'}});
if (!defined($ssid)) {
$ssid = $globals->{'SSIDCounter'}++;
}
# Setup data and conversions
$globals->{'SSIDMap'}->{$ssid} = $globals->{'SIDSubscribers'}->{$sid}->{$ssid} = {
'SID' => $sid,
'SSID' => $ssid,
'Conversions' => $conversions,
'Handler' => $handler,
'Event' => $event
};
# Return the SID we subscribed
return $ssid;
}
# Handle unsubscribes
sub unsubscribe
{
my $ssid = shift;
# Grab item, and check if it doesnt exist
my $item = $globals->{'SSIDMap'}->{$ssid};
if (!defined($item)) {
$logger->log(LOG_ERR,"[STATISTICS] Got unsubscription request for SSID '%s' that doesn't exist",
$ssid
);
return
}
$logger->log(LOG_INFO,"[STATISTICS] Got unsubscription request for SSID '%s'",
$ssid
);
# Remove subscriber
delete($globals->{'SIDSubscribers'}->{$item->{'SID'}}->{$ssid});
# If SID is now empty, remove it too
if (! keys %{$globals->{'SIDSubscribers'}->{$item->{'SID'}}}) {
delete($globals->{'SIDSubscribers'}->{$item->{'SID'}});
}
# Remove mapping
delete($globals->{'SSIDMap'}->{$ssid});
# Push onto list of free ID's
push(@{$globals->{'SSIDCounterFreeList'}},$ssid);
}
# Return user last stats
sub getLastStats
{
my $lid = shift;
my $statistics;
# # Do we have stats for this user in the cache?
# if (defined($statsCache->{$lid})) {
# # Grab last entry
# my $lastTimestamp = (sort keys %{$statsCache->{$lid}})[-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->{$lid}->{$lastTimestamp}->{$direction}) {
# # Setup the statistics hash
# $statistics->{$direction} = {
# 'current_rate' => $stats->{'current_rate'},
# 'current_pps' => $stats->{'current_pps'},
# };
# }
# }
# }
# }
return $statistics;
}
# Return stats by SID
sub getStatsBySID
{
my ($sid,$conversions,$startTimestamp,$endTimestamp) = @_;
my $statistics = _getStatsBySID($sid,$startTimestamp,$endTimestamp);
if (!defined($statistics)) {
return;
}
# Loop and convert
foreach my $timestamp (keys %{$statistics}) {
my $stat = $statistics->{$timestamp};
# Use new item
$statistics->{$timestamp} = _fixStatDirection($stat,$conversions);
}
return $statistics;
}
# Return basic stats by SID
sub getStatsBasicBySID
{
my ($sid,$conversions) = @_;
my $statistics = _getStatsBasicBySID($sid);
if (!defined($statistics)) {
return;
}
# Loop and convert
foreach my $timestamp (keys %{$statistics}) {
my $stat = $statistics->{$timestamp};
# Use new item
$statistics->{$timestamp} = _fixCounterName($stat,$conversions);
}
return $statistics;
}
# Get the stats ID from Class ID
sub getSIDFromCID
{
my ($iface,$cid) = @_;
# Grab identifier based on class ID
my $identifier = _getIdentifierFromCID($iface,$cid);
if (!defined($identifier)) {
return;
}
# Return the SID fo the identifier
return _getSIDFromIdentifier($identifier);
}
# Set the stats ID from Class ID
sub setSIDFromCID
{
my ($iface,$cid) = @_;
# See if we can get a SID from the CID
my $sid = getSIDFromCID($iface,$cid);
if (!defined($sid)) {
# If not, grab the identifier
my $identifier = _getIdentifierFromCID($iface,$cid);
if (!defined($identifier)) {
return;
}
# And setup a new SID
$sid = _setSIDFromIdentifier($identifier);
}
return $sid;
}
# Get the stats ID from a PID
sub getSIDFromPID
{
my $pid = shift;
# Grab identifier from a PID
my $identifier = _getIdentifierFromPID($pid);
if (!defined($identifier)) {
return;
}
# Return the SID for the PID
return _getSIDFromIdentifier($identifier);
}
# Set the stats ID from a PID
sub setSIDFromPID
{
my $pid = shift;
# Try grab the SID for the PID
my $sid = getSIDFromPID($pid);
if (!defined($sid)) {
# If we can't, grab the identifier instead
my $identifier = _getIdentifierFromPID($pid);
if (!defined($identifier)) {
return;
}
# And setup the SID
$sid = _setSIDFromIdentifier($identifier);
}
return $sid;
}
# Get the stats ID from a counter
sub getSIDFromCounter
{
my $counter = shift;
# Grab identifier from a counter
my $identifier = _getIdentifierFromCounter($counter);
if (!defined($identifier)) {
return;
}
# Return the SID for the counter
return _getSIDFromIdentifier($identifier);
}
# Set the stats ID from a counter
sub setSIDFromCounter
{
my $counter = shift;
# Try grab the SID for the counter
my $sid = getSIDFromCounter($counter);
if (!defined($sid)) {
# If we can't, grab the identifier instead
my $identifier = _getIdentifierFromCounter($counter);
if (!defined($identifier)) {
return;
}
# And setup the SID
$sid = _setSIDFromIdentifier($identifier);
}
return $sid;
}
# Return traffic direction
sub getTrafficDirection
{
my ($pid,$interface) = @_;
# Grab the interfaces for this limit
my $txInterface = getPoolTxInterface($pid);
my $rxInterface = getPoolRxInterface($pid);
# Check what it matches...
if ($interface eq $txInterface) {
return STATISTICS_DIR_TX;
} elsif ($interface eq $rxInterface) {
return STATISTICS_DIR_RX;
}
return;
}
# Generate ConfigManager counters
sub getConfigManagerCounters
{
my @poolList = getPools();
my @classes = getAllTrafficClasses();
# Grab user count
my %counters;
$counters{"configmanager.totalpools"} = @poolList;
# Zero this counter
$counters{"configmanager.totalpoolmembers"} = 0;
# Zero the number of pools in each class to start off with
foreach my $cid (@classes) {
$counters{"configmanager.classpools.$cid"} = 0;
$counters{"configmanager.classpoolmembers.$cid"} = 0;
}
# Pull in each pool and bump up the class counter
foreach my $pid (@poolList) {
my $pool = getPool($pid);
my $cid = getPoolTrafficClassID($pid);
my @poolMembers = getPoolMembers($pid);
# Bump the class counters
$counters{"configmanager.classpools.$cid"}++;
$counters{"configmanager.classpoolmembers.$cid"} += @poolMembers;
# Bump the pool member counter
$counters{"configmanager.totalpoolmembers"} += @poolMembers;
# Set pool member count
$counters{"configmanager.poolmembers.$pool->{'InterfaceGroupID'}/$pool->{'Name'}"} = @poolMembers;
}
return \%counters;
}
#
# Internal Functions
#
# Function to process a bunch of statistics
sub _processStatistics
{
my ($kernel,$statsData) = @_;
my $queuedEvents;
# Loop through stats data we got
while ((my $sid, my $stat) = each(%{$statsData})) {
$stat->{'IdentifierID'} = $sid;
$stat->{'Key'} = 0;
# Add to main queue
push(@{$globals->{'StatsQueue'}},$stat);
# Check if we have an event handler subscriber for this item
if (defined(my $subscribers = $globals->{'SIDSubscribers'}->{$sid})) {
# Build the stat that our conversions understands
my $eventStat;
# This is a basic counter
if (defined($stat->{'Counter'})) {
$eventStat = {
'counter' => $stat->{'Counter'}
};
} else {
$eventStat->{$stat->{'Direction'}} = {
'rate' => $stat->{'Rate'},
'pps' => $stat->{'PPS'},
'cir' => $stat->{'CIR'},
'limit' => $stat->{'Limit'}
};
}
# If we do, loop with them
foreach my $ssid (keys %{$subscribers}) {
my $subscriber = $subscribers->{$ssid};
my $handler = $subscriber->{'Handler'};
my $event = $subscriber->{'Event'};
my $conversions = $subscriber->{'Conversions'};
# Get temp stat, this still refs the original one
my $tempStat;
# This is a basic counter
if (defined($eventStat->{'counter'})) {
$tempStat = _fixCounterName($eventStat,$conversions);
} else {
$tempStat = _fixStatDirection($eventStat,$conversions);
}
# Send a copy! so we don't send refs to data used elsewhere
$queuedEvents->{$handler}->{$event}->{$ssid}->{$stat->{'Timestamp'}} = dclone($tempStat);
}
}
}
# Loop with events we need to dispatch
foreach my $handler (keys %{$queuedEvents}) {
my $events = $queuedEvents->{$handler};
foreach my $event (keys %{$events}) {
$kernel->post($handler => $event => $queuedEvents->{$handler}->{$event});
}
}
}
# Generate ConfigManager stats
sub _getConfigManagerStats
{
my $counters = getConfigManagerCounters();
my $now = time();
my $statsData = { };
# Loop through counters and create stats items
foreach my $item (keys %{$counters}) {
my $identifierID = setSIDFromCounter($item);
my $stat = {
'IdentifierID' => $identifierID,
'Timestamp' => $now,
'Counter' => $counters->{$item}
};
$statsData->{$identifierID} = $stat;
}
return $statsData;
}
# Function to get a SID identifier from a class ID
sub _getIdentifierFromCID
{
my ($iface,$cid) = @_;
return sprintf("Class:%s:%s",$iface,$cid);
}
# Function to get a SID identifier from a pool ID
sub _getIdentifierFromPID
{
my $pid = shift;
my $pool = getPool($pid);
if (!defined($pool)) {
return;
}
return sprintf("Pool:%s/%s",$pool->{'InterfaceGroupID'},$pool->{'Name'});
}
# Function to get a SID identifier from a counter
sub _getIdentifierFromCounter
{
my $counter = shift;
return sprintf("Counter:%s",$counter);
}
# Return a cached SID if its cached
sub _getCachedSIDFromIdentifier
{
my $identifier = shift;
return $globals->{'IdentifierMap'}->{$identifier};
}
# Grab or add the identifier to the DB
sub _getSIDFromIdentifier
{
my $identifier = shift;
# Check if we have it cached
if (my $sid = _getCachedSIDFromIdentifier($identifier)) {
return $sid;
}
# We need the DB to be alive to do this...
if (!defined($globals->{'Database'})) {
return;
}
# Try grab it from DB
if (my $res = DBSelect(SQL_GET_IDENTIFIER,$identifier)) {
# Grab first row and return
if (my $row = $res->fetchrow_hashref()) {
DBFreeRes($res);
return $globals->{'IdentifierMap'}->{$identifier} = $row->{'id'};
}
DBFreeRes($res);
} else {
$logger->log(LOG_ERR,"[STATISTICS] Failed to get SID from identifier '%s': %s",$identifier,awitpt::db::dblayer::Error());
}
return;
}
# Set SID from identifier in DB
sub _setSIDFromIdentifier
{
my $identifier = shift;
# We need the DB to be alive to do this...
if (!defined($globals->{'Database'})) {
return;
}
# Try add it to the DB
if (my $res = DBDo(SQL_ADD_IDENTIFIER,$identifier)) {
return $globals->{'IdentifierMap'}->{$identifier} = DBLastInsertID("","");
} else {
$logger->log(LOG_ERR,"[STATISTICS] Failed to set SID from identifier '%s': %s",$identifier,awitpt::db::dblayer::Error());
}
return;
}
# Get aligned time on a Precision
sub _getAlignedTime
{
my ($time,$precision) = @_;
return $time - ($time % $precision);
}
# Internal function to get stats by SID
sub _getStatsBySID
{
my ($sid,$startTimestamp,$endTimestamp) = @_;
my $now = time();
# Setup our timestamps if we need to
if (!defined($startTimestamp)) {
$startTimestamp = $now - 3600;
}
if (!defined($endTimestamp)) {
$endTimestamp = $now;
}
# Work out the timestamp
my $timespan = $endTimestamp - $startTimestamp;
# Find the best key to use...
my $statsKey = 0;
foreach my $key (sort {$b <=> $a} keys %{STATS_CONFIG()}) {
# Grab first key that will hve 50+ entries
if ($timespan / STATS_CONFIG()->{$key}->{'precision'} > 50) {
$statsKey = $key;
last;
}
}
my $statistics = { };
# We need the DB below this point
if (!defined($globals->{'Database'})) {
return $statistics;
}
# Grab last 60 mins of data
my $res = DBSelect(SQL_GET_STATS,$sid,$statsKey,$startTimestamp,$endTimestamp);
if (!defined($res)) {
$logger->log(LOG_ERR,"[STATISTICS] Failed to get stats for SID '%s': %s",$sid,awitpt::db::dblayer::Error());
return $statistics;
}
while (my $item = $res->fetchrow_hashref()) {
$statistics->{$item->{'timestamp'}}->{$item->{'direction'}} = {
'rate' => $item->{'rate'},
'pps' => $item->{'pps'},
'cir' => $item->{'cir'},
'limit' => $item->{'limit'},
}
}
DBFreeRes($res);
return $statistics;
}
# Internal function to get basic stats by SID
sub _getStatsBasicBySID
{
my ($sid,$startTimestamp,$endTimestamp) = @_;
my $now = time();
# Setup our timestamps if we need to
if (!defined($startTimestamp)) {
$startTimestamp = $now - 3600;
}
if (!defined($endTimestamp)) {
$endTimestamp = $now;
}
# Work out the timestamp
my $timespan = $endTimestamp - $startTimestamp;
# Find the best key to use...
my $statsKey = 0;
foreach my $key (sort {$b <=> $a} keys %{STATS_CONFIG()}) {
# Grab first key that will hve 50+ entries
if ($timespan / STATS_CONFIG()->{$key}->{'precision'} > 50) {
$statsKey = $key;
last;
}
}
my $statistics = { };
# We need the DB below this point
if (!defined($globals->{'Database'})) {
return $statistics;
}
# Prepare query
my $res = DBSelect(SQL_GET_STATS_BASIC,$sid,$statsKey,$startTimestamp,$endTimestamp);
while (my $item = $res->fetchrow_hashref()) {
$statistics->{$item->{'timestamp'}} = {
'counter' => $item->{'counter'},
}
}
DBFreeRes($res);
return $statistics;
}
# Function to transform stats before sending them
sub _fixStatDirection
{
my ($stat,$conversions) = @_;
my $res;
# Loop with directions, maybe we have more than one with this stat
while ((my $direction, my $oldStat) = each(%{$stat})) {
# Depending which direction, grab the key to use below
my $oldKey;
if ($direction == STATISTICS_DIR_TX) {
$oldKey = 'tx';
} elsif ($direction == STATISTICS_DIR_RX) {
$oldKey = 'rx';
}
# Loop and remove the direction, instead, adding it to the item
foreach my $item (keys %{$oldStat}) {
# If we have conversions defined...
my $newKey;
if (defined($conversions) && defined($conversions->{'Direction'})) {
$newKey = sprintf("%s.%s",$conversions->{'Direction'},$item);
} else {
$newKey = sprintf("%s.%s",$oldKey,$item);
}
$res->{$newKey} = $oldStat->{$item};
}
}
return $res;
}
# Function to transform stats before sending them
sub _fixCounterName
{
my ($stat,$conversions) = @_;
# Loop and set the identifier
my $newStat;
# If we have conversions defined...
my $newKey = 'counter';
if (defined($conversions) && defined($conversions->{'Name'})) {
$newKey = sprintf('%s',$conversions->{'Name'});
}
$newStat->{$newKey} = $stat->{'counter'};
return $newStat;
}
1;
# vim: ts=4
# OpenTrafficShaper Linux tc traffic shaping
# Copyright (C) 2007-2013, AllWorldIT
#
# Copyright (C) 2007-2023, 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
......@@ -21,12 +21,53 @@ package opentrafficshaper::plugins::tc;
use strict;
use warnings;
use JSON;
use List::Util qw(min max);
use POE qw(
Wheel::Run Filter::Line
);
use POE;
use awitpt::util qw(
toHex
);
use opentrafficshaper::constants;
use opentrafficshaper::logger;
use opentrafficshaper::plugins::configmanager qw(
getPool
getPoolAttribute
setPoolAttribute
removePoolAttribute
getPoolTxInterface
getPoolRxInterface
setPoolShaperState
unsetPoolShaperState
getPoolShaperState
getEffectivePool
getPoolMember
setPoolMemberAttribute
getPoolMemberAttribute
removePoolMemberAttribute
getPoolMemberMatchPriority
setPoolMemberShaperState
unsetPoolMemberShaperState
getPoolMemberShaperState
getTrafficClassPriority
getAllTrafficClasses
getInterface
getInterfaceGroup
getInterfaceGroups
getInterfaces
getInterfaceDefaultPool
getEffectiveInterfaceTrafficClass2
isInterfaceTrafficClassValid
setInterfaceTrafficClassShaperState
unsetInterfaceTrafficClassShaperState
);
# Exporter stuff
......@@ -39,7 +80,19 @@ our (@ISA,@EXPORT,@EXPORT_OK);
);
use constant {
VERSION => '0.0.1',
VERSION => '1.0.1',
# 5% of a link can be used for very high priority traffic
PROTO_RATE_LIMIT => 5,
PROTO_RATE_BURST_MIN => 16, # With a minimum burst of 8KiB
PROTO_RATE_BURST_MAXM => 1.5, # Multiplier for burst min to get to burst max
# High priority traffic gets the first 20% of the bandidth to itself
PRIO_RATE_LIMIT => 20,
PRIO_RATE_BURST_MIN => 32, # With a minimum burst of 40KiB
PRIO_RATE_BURST_MAXM => 1.5, # Multiplier for burst min to get to burst max
TC_ROOT_CLASS => 1,
};
......@@ -47,87 +100,1793 @@ use constant {
our $pluginInfo = {
Name => "Linux tc Interface",
Version => VERSION,
Init => \&init,
Init => \&plugin_init,
Start => \&plugin_start,
};
# Copy of system globals
# Our globals
my $globals;
# Copy of system logger
my $logger;
# Our configuration
my $config = {
'ip_protocol' => "ip",
'iphdr_offset' => 0,
};
#
# TASK QUEUE
#
# $globals->{'TaskQueue'}
#
# TC CLASSES & FILTERS
#
# $globals->{'TcClasses'}
# $globals->{'TcFilterMappings'}
# $globals->{'TcFilters'}
# Initialize plugin
sub init
sub plugin_init
{
$globals = shift;
my $system = shift;
# Setup our environment
$logger = $globals->{'logger'};
$logger = $system->{'logger'};
$logger->log(LOG_NOTICE,"[TC] OpenTrafficShaper tc Integration v%s - Copyright (c) 2007-2023, AllWorldIT",VERSION);
# Initialize
$globals->{'TaskQueue'} = [ ];
$globals->{'TcClasses'} = { };
$globals->{'TcFilterMappings'} = { };
$globals->{'TcFilters'} = { };
# Grab some of our config we need
if (defined(my $proto = $system->{'file.config'}->{'plugin.tc'}->{'protocol'})) {
$logger->log(LOG_INFO,"[TC] Set protocol to '%s'",$proto);
$config->{'ip_protocol'} = $proto;
}
if (defined(my $offset = $system->{'file.config'}->{'plugin.tc'}->{'iphdr_offset'})) {
$logger->log(LOG_INFO,"[TC] Set IP header offset to '%s'",$offset);
$config->{'iphdr_offset'} = $offset;
}
# We going to queue the initialization in plugin initialization so nothing at all can come before us
my $changeSet = TC::ChangeSet->new();
# Loop with protocols
for my $ipv ("", "6") {
#
# Traffic Shaping
#
# First the Cleanup
#
foreach my $interfaceID (getInterfaces()) {
my $interface = getInterface($interfaceID);
# Flush inteface chain
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','mangle',
'-F',"ots-tcfor-$interfaceID",
]);
# Delete jump
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','mangle',
'-D','ots-tcfor',
'-o',$interface->{'Device'},
'-j',"ots-tcfor-$interfaceID",
]);
# Delete interface chain
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','mangle',
'-X',"ots-tcfor-$interfaceID",
]);
}
# Flush ots-tcfor
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','mangle',
'-F','ots-tcfor',
]);
# Delete jump to ots-tcfor
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','mangle',
'-D','FORWARD',
'-j','ots-tcfor',
]);
# Delete ots-tcfor
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','mangle',
'-X','ots-tcfor',
]);
# Then the setup
#
# Create ots-tcfor
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','mangle',
'-N','ots-tcfor',
]);
# Add jump to ots-tcfor
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','mangle',
'-I','FORWARD',
'-j','ots-tcfor',
]);
# Add interface chains
foreach my $interfaceID (getInterfaces()) {
my $interface = getInterface($interfaceID);
# Create ots-tcfor
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','mangle',
'-N',"ots-tcfor-$interfaceID",
]);
# Add jump to ots-tcfor
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','mangle',
'-A','ots-tcfor',
'-o',$interface->{'Device'},
'-j',"ots-tcfor-$interfaceID",
]);
}
#
# NAT
#
# First the Cleanup
#
foreach my $interfaceGroupName (getInterfaceGroups()) {
my $interfaceGroup = getInterfaceGroup($interfaceGroupName);
my $txInterfaceID = $interfaceGroup->{'TxInterface'};
my $rxInterfaceID = $interfaceGroup->{'RxInterface'};
my $txInterface = getInterface($txInterfaceID);
my $rxInterface = getInterface($rxInterfaceID);
# Flush inteface chains
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-F',"ots-snat-$txInterfaceID",
]);
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-F',"ots-dnat-$rxInterfaceID",
]);
# Delete interface chains
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-X',"ots-snat-$txInterfaceID",
]);
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-X',"ots-dnat-$rxInterfaceID",
]);
}
# Flush ots-snat and ots-dnat
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-F','ots-snat',
]);
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-F','ots-dnat',
]);
# Delete jump to ots-snat and ots-dnat
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-D','POSTROUTING',
'-j','ots-snat',
]);
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-D','OUTPUT',
'-j','ots-dnat',
]);
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-D','PREROUTING',
'-j','ots-dnat',
]);
# Delete ots-tcfor
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-X','ots-snat',
]);
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-X','ots-dnat',
]);
# Then the setup
#
# Create ots-snat and ots-dnat
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-N','ots-snat',
]);
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-N','ots-dnat',
]);
# Add jump to ots-snat and ots-dnat
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-I','POSTROUTING',
'-j','ots-snat',
]);
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-I','OUTPUT',
'-j','ots-dnat',
]);
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-A','PREROUTING',
'-j','ots-dnat',
]);
foreach my $interfaceGroupName (getInterfaceGroups()) {
my $interfaceGroup = getInterfaceGroup($interfaceGroupName);
my $txInterfaceID = $interfaceGroup->{'TxInterface'};
my $rxInterfaceID = $interfaceGroup->{'RxInterface'};
my $txInterface = getInterface($txInterfaceID);
my $rxInterface = getInterface($rxInterfaceID);
# Flush inteface chain
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-N',"ots-snat-$txInterfaceID",
]);
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-N',"ots-dnat-$rxInterfaceID",
]);
# Add interface chains
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-A','ots-snat',
'-o',$rxInterface->{'Device'},
'-j',"ots-snat-$txInterfaceID",
]);
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-A','ots-snat',
'-o',$txInterface->{'Device'},
'-j',"ots-snat-$txInterfaceID",
]);
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-A','ots-dnat',
'-i',$rxInterface->{'Device'},
'-j',"ots-dnat-$rxInterfaceID",
]);
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-A','ots-dnat',
'-i',$txInterface->{'Device'},
'-j',"ots-dnat-$rxInterfaceID",
]);
}
}
# Loop with the configured interfaces and initialize them
foreach my $interfaceID (getInterfaces()) {
my $interface = getInterface($interfaceID);
# Initialize interface
$logger->log(LOG_INFO,"[TC] Queuing tasks to initialize '%s'",$interface->{'Device'});
_tc_iface_init($changeSet,$interfaceID);
}
_task_add_to_queue($changeSet);
# This session is our main session, its alias is "shaper"
POE::Session->create(
inline_states => {
_start => \&_session_start,
_stop => \&_session_stop,
class_change => \&_session_class_change,
pool_add => \&_session_pool_add,
pool_remove => \&_session_pool_remove,
pool_change => \&_session_pool_change,
poolmember_add => \&_session_poolmember_add,
poolmember_remove => \&_session_poolmember_remove,
}
);
# This is our configuration processing session
# This is our session for communicating directly with tc, its alias is _tc
POE::Session->create(
inline_states => {
_start => \&session_init,
add => \&do_add,
change => \&do_change,
remove => \&do_remove,
_start => \&_task_session_start,
_stop => sub { },
# Signals
_SIGCHLD => \&_task_SIGCHLD,
_SIGINT => \&_task_SIGINT,
# Public'ish
queue => \&_task_queue,
# Internal
_task_child_stdout => \&_task_child_stdout,
_task_child_stderr => \&_task_child_stderr,
_task_child_stdin => \&_task_child_stdin,
_task_child_close => \&_task_child_close,
_task_child_error => \&_task_child_error,
_task_run_next => \&_task_run_next,
}
);
$logger->log(LOG_NOTICE,"[TC] OpenTrafficShaper tc Integration v".VERSION." - Copyright (c) 2013, AllWorldIT")
return 1;
}
# Initialize config manager
sub session_init {
my $kernel = $_[KERNEL];
# Start the plugin
sub plugin_start
{
$logger->log(LOG_INFO,"[TC] Started");
}
# Initialize this plugins main POE session
sub _session_start
{
my ($kernel,$heap) = @_[KERNEL,HEAP];
# Set our alias
$kernel->alias_set("shaper");
$logger->log(LOG_DEBUG,"[TC] Initialized");
}
# Initialize this plugins main POE session
sub _session_stop
{
my ($kernel,$heap) = @_[KERNEL,HEAP];
# Remove our alias
$kernel->alias_remove("shaper");
# Blow away data
$globals = undef;
$logger->log(LOG_DEBUG,"[TC] Shutdown");
$logger = undef;
}
# Event handler for changing a class
sub _session_class_change
{
my ($kernel, $interfaceTrafficClassID) = @_[KERNEL, ARG0, ARG1];
# Grab our effective class
my $effectiveInterfaceTrafficClass = getEffectiveInterfaceTrafficClass2($interfaceTrafficClassID);
# Grab interface ID
my $interfaceID = $effectiveInterfaceTrafficClass->{'InterfaceID'};
# Grab interface from config manager
my $interface = getInterface($interfaceID);
# Grab traffic class ID
my $trafficClassID = $effectiveInterfaceTrafficClass->{'TrafficClassID'};
$logger->log(LOG_INFO,"[TC] Processing interface class changes for '%s' traffic class ID '%s'",
$interface->{'Device'},
$trafficClassID
);
# Grab tc interface
my $tcInterface = $globals->{'Interfaces'}->{$interfaceID};
# Grab interface traffic class
my $interfaceTrafficClass = $tcInterface->{'TrafficClasses'}->{$trafficClassID};
# Grab the traffic class
my $majorTcClass = $tcInterface->{'TcClass'};
my $minorTcClass = $interfaceTrafficClass->{"TcClass"};
# Generate changeset
my $changeSet = TC::ChangeSet->new();
# If we're a normal class we are treated differently than if we're a main/root class below (interface main speed)
if ($minorTcClass > 1) {
_tc_class_change($changeSet,$interfaceID,$majorTcClass,"",$minorTcClass,
$effectiveInterfaceTrafficClass->{'CIR'},
$effectiveInterfaceTrafficClass->{'Limit'}
);
# XXX: This will be the actual interface, we set limit and burst to the same
} else {
_tc_class_change($changeSet,$interfaceID,TC_ROOT_CLASS,"",$minorTcClass,$effectiveInterfaceTrafficClass->{'Limit'});
}
# Post changeset
$kernel->post("_tc" => "queue" => $changeSet);
# Mark as live
unsetInterfaceTrafficClassShaperState($interfaceTrafficClassID,SHAPER_NOTLIVE|SHAPER_PENDING);
setInterfaceTrafficClassShaperState($interfaceTrafficClassID,SHAPER_LIVE);
}
# Event handler for adding a pool
sub _session_pool_add
{
my ($kernel,$heap,$pid) = @_[KERNEL, HEAP, ARG0];
# Grab pool
my $pool;
if (!defined($pool = getPool($pid))) {
$logger->log(LOG_ERR,"[TC] Shaper 'add' event with non existing pool '%s'",$pid);
return;
}
$logger->log(LOG_INFO,"[TC] Add pool '%s' [%s] to interface group '%s'",
$pool->{'Name'},
$pool->{'ID'},
$pool->{'InterfaceGroupID'},
);
# Grab our effective pool
my $effectivePool = getEffectivePool($pool->{'ID'});
my $changeSet = TC::ChangeSet->new();
# Grab some things we need from the main pool
my $txInterfaceID = getPoolTxInterface($pool->{'ID'});
my $rxInterfaceID = getPoolRxInterface($pool->{'ID'});
# Grab effective config
my $trafficClassID = $effectivePool->{'TrafficClassID'};
my $txCIR = $effectivePool->{'TxCIR'};
my $txLimit = $effectivePool->{'TxLimit'};
my $rxCIR = $effectivePool->{'RxCIR'};
my $rxLimit = $effectivePool->{'RxLimit'};
my $trafficPriority = getTrafficClassPriority($effectivePool->{'TrafficClassID'});
# Get the Tx traffic classes TC class
my $tcClass_TxTrafficClass = _getTcClassFromTrafficClassID($txInterfaceID,$trafficClassID);
# Generate our pools Tx TC class
my $tcClass_TxPool = _reserveMinorTcClassByPoolID($txInterfaceID,$pool->{'ID'});
# Add the main Tx TC class for this pool
_tc_class_add($changeSet,$txInterfaceID,TC_ROOT_CLASS,$tcClass_TxTrafficClass,$tcClass_TxPool,$txCIR,
$txLimit,$trafficPriority
);
# Add Tx TC optimizations
_tc_class_optimize($changeSet,$txInterfaceID,$tcClass_TxPool,$txCIR);
# Set Tx TC class
setPoolAttribute($pool->{'ID'},'tc.txclass',$tcClass_TxPool);
# Get the Rx traffic classes TC class
my $tcClass_RxTrafficClass = _getTcClassFromTrafficClassID($rxInterfaceID,$trafficClassID);
# Generate our pools Rx TC class
my $tcClass_RxPool = _reserveMinorTcClassByPoolID($rxInterfaceID,$pool->{'ID'});
# Add the main Rx TC class for this pool
_tc_class_add($changeSet,$rxInterfaceID,TC_ROOT_CLASS,$tcClass_RxTrafficClass,$tcClass_RxPool,$rxCIR,
$rxLimit,$trafficPriority
);
# Add Rx TC optimizations
_tc_class_optimize($changeSet,$rxInterfaceID,$tcClass_RxPool,$rxCIR);
# Set Rx TC
setPoolAttribute($pool->{'ID'},'tc.rxclass',$tcClass_RxPool);
# Post changeset
$kernel->post("_tc" => "queue" => $changeSet);
# Set current live values
setPoolAttribute($pool->{'ID'},'shaper.live.ClassID',$trafficClassID);
setPoolAttribute($pool->{'ID'},'shaper.live.TxCIR',$txCIR);
setPoolAttribute($pool->{'ID'},'shaper.live.TxLimit',$txLimit);
setPoolAttribute($pool->{'ID'},'shaper.live.RxCIR',$rxCIR);
setPoolAttribute($pool->{'ID'},'shaper.live.RxLimit',$rxLimit);
# Mark as live
unsetPoolShaperState($pool->{'ID'},SHAPER_NOTLIVE|SHAPER_PENDING);
setPoolShaperState($pool->{'ID'},SHAPER_LIVE);
}
# Event handler for removing a pool
sub _session_pool_remove
{
my ($kernel, $pid) = @_[KERNEL, ARG0];
my $changeSet = TC::ChangeSet->new();
# Pull in pool
my $pool;
if (!defined($pool = getPool($pid))) {
$logger->log(LOG_ERR,"[TC] Shaper 'remove' event with non existing pool '%s'",$pid);
return;
}
# Make sure its not NOTLIVE
if (getPoolShaperState($pid) & SHAPER_NOTLIVE) {
$logger->log(LOG_WARN,"[TC] Ignoring remove for pool '%s' [%s]",
$pool->{'Name'},
$pool->{'ID'}
);
return;
}
$logger->log(LOG_INFO,"[TC] Removing pool '%s' [%s]",
$pool->{'Name'},
$pool->{'ID'}
);
# Grab our interfaces
my $txInterfaceID = getPoolTxInterface($pool->{'ID'});
my $rxInterfaceID = getPoolRxInterface($pool->{'ID'});
# Grab the traffic class from the pool
my $txPoolTcClass = getPoolAttribute($pool->{'ID'},'tc.txclass');
my $rxPoolTcClass = getPoolAttribute($pool->{'ID'},'tc.rxclass');
# Grab current class ID
my $trafficClassID = getPoolAttribute($pool->{'ID'},'shaper.live.ClassID');
# Grab our minor classes
my $txTrafficClassTcClass = _getTcClassFromTrafficClassID($txInterfaceID,$trafficClassID);
my $rxTrafficClassTcClass = _getTcClassFromTrafficClassID($rxInterfaceID,$trafficClassID);
my $txInterface = getInterface($txInterfaceID);
my $rxInterface = getInterface($rxInterfaceID);
# Clear up the class
$changeSet->add([
'/sbin/tc','class','del',
'dev',$txInterface->{'Device'},
'parent',"1:$txTrafficClassTcClass",
'classid',"1:$txPoolTcClass",
]);
$changeSet->add([
'/sbin/tc','class','del',
'dev',$rxInterface->{'Device'},
'parent',"1:$rxTrafficClassTcClass",
'classid',"1:$rxPoolTcClass",
]);
# And recycle the classs
_disposePoolTcClass($txInterface->{'Device'},$txPoolTcClass);
_disposePoolTcClass($rxInterface->{'Device'},$rxPoolTcClass);
_disposePrioTcClass($txInterface->{'Device'},$txPoolTcClass);
_disposePrioTcClass($rxInterface->{'Device'},$rxPoolTcClass);
# Post changeset
$kernel->post("_tc" => "queue" => $changeSet);
# Cleanup attributes
removePoolAttribute($pool->{'ID'},'tc.txclass');
removePoolAttribute($pool->{'ID'},'tc.rxclass');
removePoolAttribute($pool->{'ID'},'shaper.live.ClassID');
removePoolAttribute($pool->{'ID'},'shaper.live.TxCIR');
removePoolAttribute($pool->{'ID'},'shaper.live.TxLimit');
removePoolAttribute($pool->{'ID'},'shaper.live.RxCIR');
removePoolAttribute($pool->{'ID'},'shaper.live.RxLimit');
# Mark as not live
unsetPoolShaperState($pool->{'ID'},SHAPER_LIVE|SHAPER_PENDING);
setPoolShaperState($pool->{'ID'},SHAPER_NOTLIVE);
}
## Event handler for changing a pool
sub _session_pool_change
{
my ($kernel, $pid) = @_[KERNEL, ARG0];
# Grab pool
my $pool = getPool($pid);
$logger->log(LOG_INFO,"[TC] Processing changes for '%s' [%s]",$pool->{'Name'},$pool->{'ID'});
# Grab our effective pool
my $effectivePool = getEffectivePool($pool->{'ID'});
# Grab our interfaces
my $txInterfaceID = getPoolTxInterface($pool->{'ID'});
my $rxInterfaceID = getPoolRxInterface($pool->{'ID'});
# Grab the traffic class from the pool
my $txPoolTcClass = getPoolAttribute($pool->{'ID'},'tc.txclass');
my $rxPoolTcClass = getPoolAttribute($pool->{'ID'},'tc.rxclass');
# Grab effective config
my $trafficClassID = $effectivePool->{'TrafficClassID'};
my $txCIR = $effectivePool->{'TxCIR'};
my $txLimit = $effectivePool->{'TxLimit'};
my $rxCIR = $effectivePool->{'RxCIR'};
my $rxLimit = $effectivePool->{'RxLimit'};
my $trafficPriority = getTrafficClassPriority($trafficClassID);
# Grab our minor classes
my $txTrafficClassTcClass = _getTcClassFromTrafficClassID($txInterfaceID,$trafficClassID);
my $rxTrafficClassTcClass = _getTcClassFromTrafficClassID($rxInterfaceID,$trafficClassID);
# Generate changeset
my $changeSet = TC::ChangeSet->new();
_tc_class_change($changeSet,$txInterfaceID,TC_ROOT_CLASS,$txTrafficClassTcClass,$txPoolTcClass,$txCIR,
$txLimit,$trafficPriority);
_tc_class_change($changeSet,$rxInterfaceID,TC_ROOT_CLASS,$rxTrafficClassTcClass,$rxPoolTcClass,$rxCIR,
$rxLimit,$trafficPriority);
# Post changeset
$kernel->post("_tc" => "queue" => $changeSet);
setPoolAttribute($pool->{'ID'},'shaper.live.ClassID',$trafficClassID);
setPoolAttribute($pool->{'ID'},'shaper.live.TxCIR',$txCIR);
setPoolAttribute($pool->{'ID'},'shaper.live.TxLimit',$txLimit);
setPoolAttribute($pool->{'ID'},'shaper.live.RxCIR',$rxCIR);
setPoolAttribute($pool->{'ID'},'shaper.live.RxLimit',$rxLimit);
# Mark as live
unsetPoolShaperState($pool->{'ID'},SHAPER_NOTLIVE|SHAPER_PENDING);
setPoolShaperState($pool->{'ID'},SHAPER_LIVE);
}
# Event handler for adding a pool member
sub _session_poolmember_add
{
my ($kernel,$heap,$pmid) = @_[KERNEL, HEAP, ARG0];
# Grab pool
my $poolMember;
if (!defined($poolMember = getPoolMember($pmid))) {
$logger->log(LOG_ERR,"[TC] Shaper 'add' event with non existing pool member '%s'",$pmid);
return;
}
# Grab the pool members associated pool
my $pool;
if (!defined($pool = getPool($poolMember->{'PoolID'}))) {
$logger->log(LOG_ERR,"[TC] Shaper 'poolmember_add' event with invalid PoolID");
return;
}
$logger->log(LOG_INFO,"[TC] Add pool member '%s' [%s] with IP '%s', NAT '%s' (inbound: %s) to pool '%s' [%s]",
$poolMember->{'Username'},
$poolMember->{'ID'},
$poolMember->{'IPAddress'},
$poolMember->{'IPNATAddress'} // "",
$poolMember->{'IPNATInbound'} // "",
$pool->{'Name'},
$pool->{'ID'}
);
my $changeSet = TC::ChangeSet->new();
my $txInterfaceID = getPoolTxInterface($pool->{'ID'});
my $rxInterfaceID = getPoolRxInterface($pool->{'ID'});
my $rxPoolTcClass = getPoolAttribute($pool->{'ID'},'tc.rxclass');
my $txPoolTcClass = getPoolAttribute($pool->{'ID'},'tc.txclass');
# Check what IP version we're dealing with
my $ipv = "";
if ($poolMember->{'IPAddress'} =~ /:/) {
$ipv = "6";
}
# Add traffic classification
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','mangle',
'-A',"ots-tcfor-$rxInterfaceID",
'-s',$poolMember->{'IPAddress'},
'-m','comment',
'--comment', "pool: ".$pool->{'Name'}.", member: ".$poolMember->{'Username'},
'-j','CLASSIFY',
'--set-class',"1:$rxPoolTcClass",
]);
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','mangle',
'-A',"ots-tcfor-$txInterfaceID",
'-d',$poolMember->{'IPAddress'},
'-m','comment',
'--comment', "pool: ".$pool->{'Name'}.", member: ".$poolMember->{'Username'},
'-j','CLASSIFY',
'--set-class',"1:$txPoolTcClass",
]);
# Add NAT if we have any
if (defined($poolMember->{'IPNATAddress'}) && defined($poolMember->{'IPNATAddress'}) ne "") {
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-A',"ots-snat-$txInterfaceID",
'-s',$poolMember->{'IPAddress'},
'-m','comment',
'--comment', "pool: ".$pool->{'Name'}.", member: ".$poolMember->{'Username'},
'-j','SNAT',
'--to-source', $poolMember->{'IPNATAddress'},
]);
# If we're dealing with IPv4, clear the connection tracking
if ($ipv eq "") {
$changeSet->add(["/sbin/conntrack",'-D','-s',$poolMember->{'IPAddress'}]);
}
if (defined($poolMember->{'IPNATInbound'}) && $poolMember->{'IPNATInbound'} eq "yes") {
my @ipComponents = split(/\//,$poolMember->{'IPAddress'});
my $dnatTo = $ipComponents[0];
if (@ipComponents < 2 || $ipComponents[1] eq "32" || $ipComponents[1] eq "128") {
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-A',"ots-dnat-$rxInterfaceID",
'-d',$poolMember->{'IPNATAddress'},
'-m','comment',
'--comment', "pool: ".$pool->{'Name'}.", member: ".$poolMember->{'Username'},
'-j','DNAT',
'--to-destination', $dnatTo,
]);
} else {
$logger->log(LOG_WARN,"[TC] Cannot add inbound NAT for pool member '%s' [%s] with IP '%s', NAT '%s' (inbound: %s) to pool '%s' [%s]",
$poolMember->{'Username'},
$poolMember->{'ID'},
$poolMember->{'IPAddress'},
$poolMember->{'IPNATAddress'} // "",
$poolMember->{'IPNATInbound'} // "",
$pool->{'Name'},
$pool->{'ID'}
);
}
}
}
# Post changeset
$kernel->post("_tc" => "queue" => $changeSet);
# Mark pool member as live
unsetPoolMemberShaperState($poolMember->{'ID'},SHAPER_NOTLIVE|SHAPER_PENDING);
setPoolMemberShaperState($poolMember->{'ID'},SHAPER_LIVE);
}
# Event handler for removing a pool member
sub _session_poolmember_remove
{
my ($kernel, $pmid) = @_[KERNEL, ARG0];
# Pull in pool member
my $poolMember;
if (!defined($poolMember = getPoolMember($pmid))) {
$logger->log(LOG_ERR,"[TC] Shaper 'remove' event with non existing pool member '%s'",$pmid);
return;
}
# Grab the pool members associated pool
my $pool = getPool($poolMember->{'PoolID'});
# Make sure its not NOTLIVE
if (getPoolMemberShaperState($pmid) & SHAPER_NOTLIVE) {
$logger->log(LOG_WARN,"[TC] Ignoring remove for pool member '%s' with IP '%s', NAT '%s' (inbound: %s) [%s] from pool '%s'",
$poolMember->{'Username'},
$poolMember->{'IPAddress'},
$poolMember->{'IPNATAddress'} // "",
$poolMember->{'IPNATInbound'} // "",
$poolMember->{'ID'},
$pool->{'Name'}
);
return;
}
$logger->log(LOG_INFO,"[TC] Remove pool member '%s' [%s] with IP '%s', NAT '%s' (inbound: %s) from pool '%s' [%s]",
$poolMember->{'Username'},
$poolMember->{'ID'},
$poolMember->{'IPAddress'},
$poolMember->{'IPNATAddress'} // "",
$poolMember->{'IPNATInbound'} // "",
$pool->{'Name'},
$pool->{'ID'}
);
my $changeSet = TC::ChangeSet->new();
my $txInterfaceID = getPoolTxInterface($pool->{'ID'});
my $rxInterfaceID = getPoolRxInterface($pool->{'ID'});
my $txInterface = getInterface($txInterfaceID);
my $rxInterface = getInterface($rxInterfaceID);
my $rxPoolTcClass = getPoolAttribute($pool->{'ID'},'tc.rxclass');
my $txPoolTcClass = getPoolAttribute($pool->{'ID'},'tc.txclass');
# Check what IP version we're dealing with
my $ipv = "";
if ( $poolMember->{'IPAddress'} =~ /:/ ) {
$ipv = 6;
}
# Remove traffic classification
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','mangle',
'-D',"ots-tcfor-$rxInterfaceID",
'-s',$poolMember->{'IPAddress'},
'-m','comment',
'--comment', "pool: ".$pool->{'Name'}.", member: ".$poolMember->{'Username'},
'-j','CLASSIFY',
'--set-class',"1:$rxPoolTcClass",
]);
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','mangle',
'-D',"ots-tcfor-$txInterfaceID",
'-d',$poolMember->{'IPAddress'},
'-m','comment',
'--comment', "pool: ".$pool->{'Name'}.", member: ".$poolMember->{'Username'},
'-j','CLASSIFY',
'--set-class',"1:$txPoolTcClass",
]);
# Remove NAT if we have any
if (defined($poolMember->{'IPNATAddress'}) && defined($poolMember->{'IPNATAddress'}) ne "") {
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-D',"ots-snat-$txInterfaceID",
'-s',$poolMember->{'IPAddress'},
'-m','comment',
'--comment', "pool: ".$pool->{'Name'}.", member: ".$poolMember->{'Username'},
'-j','SNAT',
'--to-source', $poolMember->{'IPNATAddress'},
]);
if (defined($poolMember->{'IPNATInbound'}) && $poolMember->{'IPNATInbound'} eq "yes") {
my @ipComponents = split(/\//,$poolMember->{'IPAddress'});
my $dnatTo = $ipComponents[0];
if (@ipComponents < 2 || $ipComponents[1] eq "32" || $ipComponents[1] eq "128") {
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','nat',
'-D',"ots-dnat-$rxInterfaceID",
'-d',$poolMember->{'IPNATAddress'},
'-m','comment',
'--comment', "pool: ".$pool->{'Name'}.", member: ".$poolMember->{'Username'},
'-j','DNAT',
'--to-destination', $dnatTo,
]);
} else {
$logger->log(LOG_WARN,"[TC] Cannot remove inbound NAT for pool member '%s' [%s] with IP '%s', NAT '%s' (inbound: %s) to pool '%s' [%s]",
$poolMember->{'Username'},
$poolMember->{'ID'},
$poolMember->{'IPAddress'},
$poolMember->{'IPNATAddress'} // "",
$poolMember->{'IPNATInbound'} // "",
$pool->{'Name'},
$pool->{'ID'}
);
}
}
}
# Post changeset
$kernel->post("_tc" => "queue" => $changeSet);
# Mark as not live
unsetPoolMemberShaperState($poolMember->{'ID'},SHAPER_LIVE|SHAPER_PENDING);
setPoolMemberShaperState($poolMember->{'ID'},SHAPER_NOTLIVE);
}
# Grab pool ID from TC class
sub getPIDFromTcClass
{
my ($interfaceID,$majorTcClass,$minorTcClass) = @_;
# Return the pool ID if found
my $ref = __getRefByMinorTcClass($interfaceID,$majorTcClass,$minorTcClass);
if (!defined($ref) || substr($ref,0,13) ne "_pool_class_:") {
return;
}
return substr($ref,13);
}
# Function to return if this is linked to a pool's class
sub isPoolTcClass
{
my ($interfaceID,$majorTcClass,$minorTcClass) = @_;
my $pid = getPIDFromTcClass($interfaceID,$majorTcClass,$minorTcClass);
if (!defined($pid)) {
return;
}
return $minorTcClass;
}
# Return the ClassID from a TC class
# This is similar to isTcTrafficClassValid() but returns the ref, not the minor class
sub getCIDFromTcClass
{
my ($interfaceID,$majorTcClass,$minorTcClass) = @_;
# Grab ref
my $ref = __getRefByMinorTcClass($interfaceID,$majorTcClass,$minorTcClass);
# If we're undefined return
if (!defined($ref)) {
return;
}
# If we're not a traffic class, just return
if (substr($ref,0,16) ne "_traffic_class_:") {
return;
}
# Else return the part after the above tag
return substr($ref,16);
}
#
# Internal functions
#
# Function to initialize an interface
sub _tc_iface_init
{
my ($changeSet,$interfaceID) = @_;
# Grab our interface rate
my $interface = getInterface($interfaceID);
### --- Interface Setup
# Clear the qdisc from the interface
$changeSet->add([
'/sbin/tc','qdisc','del',
'dev',$interface->{'Device'},
'root',
]);
# Initialize the major TC class
my $interfaceTcClass = _reserveMajorTcClass($interfaceID,"root");
# Set interface RootClass
$globals->{'Interfaces'}->{$interfaceID} = {
'TcClass' => $interfaceTcClass
};
### --- Interface Traffic Class Setup
# Reserve our parent TC classes
my @trafficClasses = getAllTrafficClasses();
foreach my $trafficClassID (sort {$a <=> $b} @trafficClasses) {
# Record the class we get for this interface traffic class ID
my $interfaceTrafficClassTcClass = _reserveMinorTcClassByTrafficClassID($interfaceID,$trafficClassID);
}
### --- Interface Setup Part 2
# Add root qdisc
$changeSet->add([
'/sbin/tc','qdisc','add',
'dev',$interface->{'Device'},
'root',
'handle','1:',
'htb'
]);
# Attach our main limit on the qdisc
my $burst = int( ($interface->{'Limit'} / 8) * 1024 * 10); # Allow the entire interface to be emptied with a burst
$changeSet->add([
'/sbin/tc','class','add',
'dev',$interface->{'Device'},
'parent','1:',
'classid','1:1',
'htb',
'rate',"$interface->{'Limit'}kbit",
]);
# Class 0 is our interface, it points to 1 (the major TcClass)) : 1 (class below)
$globals->{'Interfaces'}->{$interfaceID}->{'TrafficClasses'}->{'0'} = {
'TcClass' => '1',
'CIR' => $interface->{'Limit'},
'Limit' => $interface->{'Limit'}
};
### --- Setup each class
# Setup the classes
foreach my $trafficClassID (@trafficClasses) {
my $interfaceTrafficClassID = isInterfaceTrafficClassValid($interfaceID,$trafficClassID);
my $interfaceTrafficClass = getEffectiveInterfaceTrafficClass2($interfaceTrafficClassID);
my $tcClass = _getTcClassFromTrafficClassID($interfaceID,$trafficClassID);
my $trafficPriority = getTrafficClassPriority($trafficClassID);
# Add class
$changeSet->add([
'/sbin/tc','class','add',
'dev',$interface->{'Device'},
'parent','1:1',
'classid',"1:$tcClass",
'htb',
'rate',"$interfaceTrafficClass->{'CIR'}kbit",
'ceil',"$interfaceTrafficClass->{'Limit'}kbit",
'prio',$trafficPriority,
]);
# Setup interface traffic class details
$globals->{'Interfaces'}->{$interfaceID}->{'TrafficClasses'}->{$trafficClassID} = {
'TcClass' => $tcClass,
'CIR' => $interfaceTrafficClass->{'CIR'},
'Limit' => $interfaceTrafficClass->{'Limit'}
};
}
# Process our default pool traffic optimizations
my $defaultPool = getInterfaceDefaultPool($interfaceID);
if (defined($defaultPool)) {
my $interfaceTrafficClassID = isInterfaceTrafficClassValid($interfaceID,$defaultPool);
my $interfaceTrafficClass = getEffectiveInterfaceTrafficClass2($interfaceTrafficClassID);
my $defaultPoolTcClass = _getTcClassFromTrafficClassID($interfaceID, $defaultPool);
# Loop with IP versions
for my $ipv ("", "6") {
$changeSet->add([
"/sbin/ip${ipv}tables",
'-t','mangle',
'-A',"ots-tcfor-$interfaceID",
'-m','comment',
'--comment', "Default",
'-j','CLASSIFY',
'--set-class',"1:$defaultPoolTcClass",
]);
}
# If we have a rate for this iface, then use it
_tc_class_optimize($changeSet,$interfaceID,$defaultPoolTcClass,$interfaceTrafficClass->{'Limit'});
}
}
# Function to apply traffic optimizations to a classes
# XXX: This probably needs working on
sub _tc_class_optimize
{
my ($changeSet,$interfaceID,$poolTcClass,$rate) = @_;
my $interface = getInterface($interfaceID);
my $prioTcClass = _reserveMajorTcClassByPrioClass($interfaceID,$poolTcClass);
# Mbyte/s rate divided by average packet size to get packet limit, with a minimum of 127
my $sfqQueueLength = max(int((($rate / 8) * 1024) / 1000), 127);
# Flows is Mbit/s divided by 20, with minimum of 127
my $sfqFlows = max(int($rate / 1000 / 20) * 127, 127);
my $sfqRedFlowLimit = ($sfqQueueLength / 4) * 1500; # Take 25% of the queue length and packet size of 1500
# We then prioritize traffic into 3 bands based on TOS
$changeSet->add([
'/sbin/tc','qdisc','add',
'dev',$interface->{'Device'},
'parent',"1:$poolTcClass",
'handle',"$prioTcClass:",
'sfq',
'limit', $sfqQueueLength, # first work out byte rate, then divide by average packet size of 1000
'flows', $sfqFlows,
'headdrop',
'perturb', 10,
'redflowlimit', $sfqRedFlowLimit,
'perturb', 10,
]);
}
# Function to add a TC class
sub _tc_class_add
{
my ($changeSet,$interfaceID,$majorTcClass,$trafficClassTcClass,$poolTcClass,$rate,$ceil,$trafficPriority) = @_;
my $interface = getInterface($interfaceID);
# # Set burst to a sane value, in this case the CIR (or $rate) size
# my $burst = int($rate / 8);
# my $cburst = int(($ceil - $rate) / 8 / 10);
# # NK: cburst should not exceed burst, if it does, just use the burst value
# # this ensures we do not get negative burst
# if ($cburst > $burst) {
# $cburst = $burst;
# }
my $burst = int( ($ceil / 8) * 1024 * 10);
my $cburst = int($rate / 8 / 5) + 2; # +2kb to cover 1600
my $quantum = int($cburst / 2) * 1024; # Burst in bytes
# Create main rate limiting classes
$changeSet->add([
'/sbin/tc','class','add',
'dev',$interface->{'Device'},
'parent',"$majorTcClass:$trafficClassTcClass",
'classid',"$majorTcClass:$poolTcClass",
'htb',
'rate', "${rate}kbit",
'ceil', "${ceil}kbit",
# 'prio', $trafficPriority,
# 'burst', "${burst}kb",
# 'cburst', "${cburst}kb",
]);
}
# Function to change a TC class
sub _tc_class_change
{
my ($changeSet,$interfaceID,$majorTcClass,$trafficClassTcClass,$poolTcClass,$rate,$ceil,$trafficPriority) = @_;
my $interface = getInterface($interfaceID);
my @args = ();
# If ceil is not available, set it to the CIR (or $rate in this case)
if (!defined($ceil)) {
$ceil = $rate;
}
# # Set the burst rate to the CIR (or $rate in this case)
# my $burst = int($rate / 8);
# my $cburst = int(($ceil - $rate) / 8 / 10);
# # NK: cburst should not exceed burst, if it does, just use the burst value
# # this ensures we do not get negative burst
# if ($cburst > $burst) {
# $cburst = $burst;
# }
my $burst = int( ($ceil / 8) * 1024 * 10);
my $cburst = int($rate / 8 / 5) + 2; # +2kb to cover 1600
my $quantum = int($cburst / 2) * 1024; # Burst in bytes
# # Check if we have a priority
# if (defined($trafficPriority)) {
# push(@args,'prio',$trafficPriority);
# }
# Create main rate limiting classes
$changeSet->add([
'/sbin/tc','class','change',
'dev',$interface->{'Device'},
'parent',"$majorTcClass:$trafficClassTcClass",
'classid',"$majorTcClass:$poolTcClass",
'htb',
'rate', "${rate}kbit",
'ceil', "${ceil}kbit",
# 'burst', "${burst}kb",
# 'cburst', "${cburst}kb",
@args
]);
}
# Get a pool TC class from pool ID
sub _reserveMinorTcClassByPoolID
{
my ($interfaceID,$pid) = @_;
return __reserveMinorTcClass($interfaceID,TC_ROOT_CLASS,"_pool_class_:$pid");
}
# Get a traffic class TC class
sub _reserveMinorTcClassByTrafficClassID
{
my ($interfaceID,$trafficClassID) = @_;
return __reserveMinorTcClass($interfaceID,TC_ROOT_CLASS,"_traffic_class_:$trafficClassID");
}
# Get a prio class TC class
# This is a MAJOR class!
sub _reserveMajorTcClassByPrioClass
{
my ($interfaceID,$trafficClassID) = @_;
return _reserveMajorTcClass($interfaceID,"_priority_class_:$trafficClassID");
}
# Return TC class from a traffic class ID
sub _getTcClassFromTrafficClassID
{
my ($interfaceID,$trafficClassID) = @_;
return __getMinorTcClassByRef($interfaceID,TC_ROOT_CLASS,"_traffic_class_:$trafficClassID");
}
# Return prio TC class using class
# This returns a MAJOR class from a tc class
sub _getPrioTcClass
{
my ($interfaceID,$tcClass) = @_;
return __getMajorTcClassByRef($interfaceID,"_priority_class_:$tcClass");
}
# Function to dispose of a TC class
sub _disposePoolTcClass
{
my ($interfaceID,$tcClass) = @_;
return __disposeMinorTcClass($interfaceID,TC_ROOT_CLASS,$tcClass);
}
# Function to dispose of a major TC class
# Uses a TC class to get a MAJOR class, then disposes it
sub _disposePrioTcClass
{
my ($interfaceID,$tcClass) = @_;
# If we can grab the major class dipose of it
my $majorTcClass = _getPrioTcClass($interfaceID,$tcClass);
if (!defined($majorTcClass)) {
return;
}
return __disposeMajorTcClass($interfaceID,$majorTcClass);
}
# Function to get next available TC class
sub __reserveMinorTcClass
{
my ($interfaceID,$majorTcClass,$ref) = @_;
# Setup defaults if we don't have anything defined
if (!defined($globals->{'TcClasses'}->{$interfaceID}) || !defined($globals->{'TcClasses'}->{$interfaceID}->{$majorTcClass})) {
$globals->{'TcClasses'}->{$interfaceID}->{$majorTcClass} = {
# Skip 0 and 1
'Counter' => 2,
'Free' => [ ],
'Track' => { },
'Reverse' => { },
};
}
# Maybe we have one free?
my $minorTcClass = shift(@{$globals->{'TcClasses'}->{$interfaceID}->{$majorTcClass}->{'Free'}});
# Generate new number
if (!$minorTcClass) {
$minorTcClass = $globals->{'TcClasses'}->{$interfaceID}->{$majorTcClass}->{'Counter'}++;
# Hex it
$minorTcClass = toHex($minorTcClass);
}
$globals->{'TcClasses'}->{$interfaceID}->{$majorTcClass}->{'Track'}->{$minorTcClass} = $ref;
$globals->{'TcClasses'}->{$interfaceID}->{$majorTcClass}->{'Reverse'}->{$ref} = $minorTcClass;
return $minorTcClass;
}
# Function to get next available major TC class
sub _reserveMajorTcClass
{
my ($interfaceID,$ref) = @_;
# Setup defaults if we don't have anything defined
if (!defined($globals->{'TcClasses'}->{$interfaceID})) {
$globals->{'TcClasses'}->{$interfaceID} = {
# Skip 0
'Counter' => 1,
'Free' => [ ],
'Track' => { },
'Reverse' => { },
};
}
# Maybe we have one free?
my $majorTcClass = shift(@{$globals->{'TcClasses'}->{$interfaceID}->{'Free'}});
# Generate new number
if (!$majorTcClass) {
$majorTcClass = $globals->{'TcClasses'}->{$interfaceID}->{'Counter'}++;
# Hex it
$majorTcClass = toHex($majorTcClass);
}
$globals->{'TcClasses'}->{$interfaceID}->{'Track'}->{$majorTcClass} = $ref;
$globals->{'TcClasses'}->{$interfaceID}->{'Reverse'}->{$ref} = $majorTcClass;
return $majorTcClass;
}
# Get a minor class by its rerf
sub __getMinorTcClassByRef
{
my ($interfaceID,$majorTcClass,$ref) = @_;
if (!defined($globals->{'TcClasses'}->{$interfaceID}) || !defined($globals->{'TcClasses'}->{$interfaceID}->{$majorTcClass})) {
return;
}
return $globals->{'TcClasses'}->{$interfaceID}->{$majorTcClass}->{'Reverse'}->{$ref};
}
# Get a major class by its rerf
sub __getMajorTcClassByRef
{
my ($interfaceID,$ref) = @_;
if (!defined($globals->{'TcClasses'}->{$interfaceID})) {
return;
}
return $globals->{'TcClasses'}->{$interfaceID}->{'Reverse'}->{$ref};
}
# Get ref using the minor tc class
sub __getRefByMinorTcClass
{
my ($interfaceID,$majorTcClass,$minorTcClass) = @_;
if (!defined($globals->{'TcClasses'}->{$interfaceID}) || !defined($globals->{'TcClasses'}->{$interfaceID}->{$majorTcClass})) {
return;
}
return $globals->{'TcClasses'}->{$interfaceID}->{$majorTcClass}->{'Track'}->{$minorTcClass};
}
# Function to dispose of a TC class
sub __disposeMinorTcClass
{
my ($interfaceID,$majorTcClass,$tcMinorClass) = @_;
my $ref = $globals->{'TcClasses'}->{$interfaceID}->{$majorTcClass}->{'Track'}->{$tcMinorClass};
# Push onto free list
push(@{$globals->{'TcClasses'}->{$interfaceID}->{$majorTcClass}->{'Free'}},$tcMinorClass);
# Blank the value
$globals->{'TcClasses'}->{$interfaceID}->{$majorTcClass}->{'Track'}->{$tcMinorClass} = undef;
delete($globals->{'TcClasses'}->{$interfaceID}->{$majorTcClass}->{'Reverse'}->{$ref});
}
# Add event for tc
sub do_add {
my ($kernel, $uid) = @_[KERNEL, ARG0];
# Function to dispose of a major TC class
sub __disposeMajorTcClass
{
my ($interfaceID,$tcMajorClass) = @_;
# Pull in global
my $users = $globals->{'users'};
my $user = $users->{$uid};
$users->{$uid}->{'shaper_live'} = SHAPER_LIVE;
$logger->log(LOG_DEBUG," Add '$user->{'Username'}' [$uid]\n");
my $ref = $globals->{'TcClasses'}->{$interfaceID}->{'Track'}->{$tcMajorClass};
# Push onto free list
push(@{$globals->{'TcClasses'}->{$interfaceID}->{'Free'}},$tcMajorClass);
# Blank the value
$globals->{'TcClasses'}->{$interfaceID}->{'Track'}->{$tcMajorClass} = undef;
delete($globals->{'TcClasses'}->{$interfaceID}->{'Reverse'}->{$ref});
}
# Change event for tc
sub do_change {
my ($kernel, $uid) = @_[KERNEL, ARG0];
# Pull in global
my $users = $globals->{'users'};
my $user = $users->{$uid};
# Function to get next available TC filter
sub _reserveTcFilter
{
my ($interfaceID,$ref) = @_;
# Setup defaults if we don't have anything defined
if (!defined($globals->{'TcFilters'}->{$interfaceID})) {
$globals->{'TcFilters'}->{$interfaceID} = {
# Skip 0 and 1
'Counter' => 2,
'Free' => [ ],
'Track' => { },
};
}
# Maybe we have one free?
my $filterID = shift(@{$globals->{'TcFilters'}->{$interfaceID}->{'Free'}});
# Generate new number
if (!$filterID) {
$filterID = $globals->{'TcFilters'}->{$interfaceID}->{'Counter'}++;
# We cannot use ID 800, its internal
$filterID = $globals->{'TcFilters'}->{$interfaceID}->{'Counter'}++ if ($filterID == 800);
# Hex it
$filterID = toHex($filterID);
}
$logger->log(LOG_DEBUG," Change '$user->{'Username'}' [$uid]\n");
$globals->{'TcFilters'}->{$interfaceID}->{'Track'}->{$filterID} = $ref;
return $filterID;
}
# Remove event for tc
sub do_remove {
my ($kernel, $uid) = @_[KERNEL, ARG0];
# Pull in global
my $users = $globals->{'users'};
my $user = $users->{$uid};
# Function to dispose of a TC Filter
sub _disposeTcFilter
{
my ($interfaceID,$filterID) = @_;
# Push onto free list
push(@{$globals->{'TcFilters'}->{$interfaceID}->{'Free'}},$filterID);
# Blank the value
$globals->{'TcFilters'}->{$interfaceID}->{'Track'}->{$filterID} = undef;
}
#
# Task/child communication & handling stuff
#
# Initialize our tc session
sub _task_session_start
{
my $kernel = $_[KERNEL];
# Set our alias
$kernel->alias_set("_tc");
# Setup handing of console INT
$kernel->sig("INT", "_SIGINT");
# Fire things up, we trigger this to process the task queue generated during init
$kernel->yield("_task_run_next");
}
# Add task to queue
sub _task_add_to_queue
{
my $changeSet = shift;
# Extract the changeset into commands
my $numChanges = 0;
foreach my $cmd ($changeSet->extract()) {
push(@{$globals->{'TaskQueue'}},$cmd);
$numChanges++;
}
$logger->log(LOG_DEBUG,"[TC] TASK: Queued %s changes",$numChanges);
}
# Queue a task
sub _task_queue
{
my ($kernel,$heap,$changeSet) = @_[KERNEL,HEAP,ARG0];
# Internal function to add command to queue
_task_add_to_queue($changeSet);
# Trigger a run if list is not empty
if (@{$globals->{'TaskQueue'}}) {
$kernel->yield("_task_run_next");
}
}
# Run next task
sub _task_run_next
{
my ($kernel,$heap) = @_[KERNEL,HEAP];
if (keys %{$heap->{'task_by_wid'}}) {
# NK: Limit concurrency to 1
return;
}
# Check if we have a task coming off the top of the task queue
if (my $cmd = shift(@{$globals->{'TaskQueue'}})) {
my $cmdStr = encode_json($cmd);
# Create task
my $task = POE::Wheel::Run->new(
Program => $cmd,
StdoutFilter => POE::Filter::Line->new( Literal => "\n" ),
StderrFilter => POE::Filter::Line->new( Literal => "\n" ),
StdoutEvent => '_task_child_stdout',
StderrEvent => '_task_child_stderr',
CloseEvent => '_task_child_close',
# ErrorEvent => '_task_child_error',
) or $logger->log(LOG_ERR,"[TC] TASK: Unable to start task: $cmdStr");
# Set task ID
my $task_id = $task->ID;
# Intercept SIGCHLD
$kernel->sig_child($task->PID, "_SIGCHLD");
# Wheel events include the wheel's ID.
$heap->{'task_by_wid'}->{$task_id} = $task;
# Signal events include the process ID.
$heap->{'task_by_pid'}->{$task->PID} = $task;
# Build commandline string
$logger->log( LOG_DEBUG, "[TC] TASK/%s: Starting '%s' as %s with PID %s", $task->ID, $cmdStr, $task->ID, $task->PID );
}
}
# Child writes to STDOUT
sub _task_child_stdout
{
my ($kernel,$heap,$stdout,$task_id) = @_[KERNEL,HEAP,ARG0,ARG1];
my $task = $heap->{'task_by_wid'}->{$task_id};
$logger->log(LOG_INFO,"[TC] TASK/%s: STDOUT => %s",$task_id,$stdout);
}
# Child writes to STDERR
sub _task_child_stderr
{
my ($kernel,$heap,$stdout,$task_id) = @_[KERNEL,HEAP,ARG0,ARG1];
my $task = $heap->{'task_by_wid'}->{$task_id};
$logger->log(LOG_WARN,"[TC] TASK/%s: STDERR => %s",$task_id,$stdout);
}
# Child closed its handles, it won't communicate with us, so remove it
sub _task_child_close
{
my ($kernel,$heap,$task_id) = @_[KERNEL,HEAP,ARG0];
my $task = $heap->{'task_by_wid'}->{$task_id};
# May have been reaped by task_sigchld()
if (!defined($task)) {
$logger->log(LOG_DEBUG,"[TC] TASK/%s: Closed dead child",$task_id);
return;
}
$logger->log(LOG_DEBUG,"[TC] TASK/%s: Closed PID %s",$task_id,$task->PID);
# Remove other references
delete($heap->{'task_by_wid'}->{$task_id});
delete($heap->{'task_by_pid'}->{$task->PID});
# Start next one, if there is a next one
if (@{$globals->{'TaskQueue'}}) {
$kernel->yield("_task_run_next");
}
}
# Child got an error event, lets remove it too
sub _task_child_error
{
my ($kernel,$heap,$operation,$errnum,$errstr,$task_id) = @_[KERNEL,HEAP,ARG0..ARG3];
my $task = $heap->{'task_by_wid'}->{$task_id};
if ($operation eq "read" && !$errnum) {
$errstr = "Remote end closed"
}
$logger->log(LOG_ERR,"[TC] Task %s generated %s error %s: '%s'",$task_id,$operation,$errnum,$errstr);
# If there is no task, return
return if (!defined($task));
# Remove other references
delete($heap->{'task_by_wid'}->{$task_id});
delete($heap->{'task_by_pid'}->{$task->PID});
# Start next one, if there is a next one
if (@{$globals->{'TaskQueue'}}) {
$kernel->yield("_task_run_next");
}
}
# Reap the dead child
sub _task_SIGCHLD
{
my ($kernel,$heap,$pid,$status) = @_[KERNEL,HEAP,ARG1,ARG2];
my $task = $heap->{'task_by_pid'}->{$pid};
$logger->log(LOG_DEBUG,"[TC] TASK: Task with PID %s exited with status %s",$pid,$status);
# May have been reaped by task_child_close()
return if (!defined($task));
# Remove other references
delete($heap->{'task_by_wid'}->{$task->ID});
delete($heap->{'task_by_pid'}->{$pid});
}
# Handle SIGINT
sub _task_SIGINT
{
my ($kernel,$heap,$signal_name) = @_[KERNEL,HEAP,ARG0];
# Shutdown stdin on all children, this will terminate /sbin/tc
foreach my $task_id (keys %{$heap->{'task_by_wid'}}) {
my $task = $heap->{'task_by_wid'}{$task_id};
# $kernel->sig_child($task->PID, "asig_child");
# $task->kill("INT"); #NK: doesn't work
$kernel->post($task,"shutdown_stdin"); #NK: doesn't work
}
$logger->log(LOG_WARN,"[TC] Killed children processes");
}
# TC changeset item
package TC::ChangeSet;
use strict;
use warnings;
# Create object
sub new
{
my $class = shift;
my $self = {
'list' => [ ]
};
bless $self, $class;
return $self;
}
# Add a change to the list
sub add
{
my ($self,$change) = @_;
push(@{$self->{'list'}},$change);
}
# Return the list
sub extract
{
my $self = shift;
return @{$self->{'list'}};
}
# Return the list
sub debug
{
my $self = shift;
my @debug = ();
foreach my $item ($self->extract) {
push(@debug,join(' ',@{$item}));
}
$users->{$uid}->{'shaper_live'} = 0;
$logger->log(LOG_DEBUG," Remove '$user->{'Username'}' [$uid]\n");
return @debug;
}
......
# OpenTrafficShaper Linux tcstats traffic shaping statistics
# Copyright (C) 2007-2023, 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::tcstats;
use strict;
use warnings;
use POE qw( Wheel::Run Filter::Line );
use opentrafficshaper::POE::Filter::TCStatistics;
use opentrafficshaper::constants;
use opentrafficshaper::logger;
use opentrafficshaper::plugins::configmanager qw(
getInterface
getInterfaces
);
# Exporter stuff
require Exporter;
our (@ISA,@EXPORT,@EXPORT_OK);
@ISA = qw(Exporter);
@EXPORT = qw(
);
@EXPORT_OK = qw(
);
use constant {
VERSION => '1.0.0',
# How often we tick
TICK_PERIOD => 5,
};
# Plugin info
our $pluginInfo = {
Name => "Linux tc Statistics Interface",
Version => VERSION,
Init => \&plugin_init,
Start => \&plugin_start,
Requires => ["tc","statistics"],
};
# Our globals
my $globals;
# Copy of system logger
my $logger;
# Last stats pulls
#
# $globals->{'LastStats'}
# Initialize plugin
sub plugin_init
{
my $system = shift;
# Setup our environment
$logger = $system->{'logger'};
$logger->log(LOG_NOTICE,"[TCSTATS] OpenTrafficShaper tc Statistics Integration v%s - Copyright (c) 2013-2014, AllWorldIT",
VERSION
);
# Initialize
$globals->{'LastStats'} = { };
# This session is our main session, its alias is "shaper"
POE::Session->create(
inline_states => {
_start => \&_session_start,
_stop => \&_session_stop,
_tick => \&_session_tick,
_task_child_stdout => \&_task_child_stdout,
_task_child_stderr => \&_task_child_stderr,
_task_child_close => \&_task_child_close,
_SIGCHLD => \&_task_handle_SIGCHLD,
_SIGINT => \&_task_handle_SIGINT,
}
);
return 1;
}
# Start the plugin
sub plugin_start
{
my @interfaces = getInterfaces();
my $now = time();
# Initialize last stats
foreach my $interfaceID (@interfaces) {
$globals->{'LastStats'}->{$interfaceID} = $now;
}
$logger->log(LOG_INFO,"[TCSTATS] Started");
}
# Initialize this plugins main POE session
sub _session_start
{
my ($kernel,$heap) = @_[KERNEL,HEAP];
# Set our alias
$kernel->alias_set("tcstats");
# Set delay on config updates
$kernel->delay('_tick' => TICK_PERIOD);
$logger->log(LOG_DEBUG,"[TCSTATS] Initialized");
}
# Shut down session
sub _session_stop
{
my ($kernel,$heap) = @_[KERNEL,HEAP];
$kernel->alias_remove("tcstats");
# Blow everything away
$globals = undef;
$logger->log(LOG_DEBUG,"[TCSTATS] Shutdown");
$logger = undef;
}
# Time ticker for processing changes
sub _session_tick
{
my ($kernel,$heap) = @_[KERNEL,HEAP];
# Now
my $now = time();
# Get sorted list of interfaces
my @interfaces = sort { $globals->{'LastStats'}->{$a} <=> $globals->{'LastStats'}->{$b} } getInterfaces();
# Grab the first interface in the list to process
my $interfaceID = shift(@interfaces);
# Check if its old enough to process stats for
if ($now - $globals->{'LastStats'}->{$interfaceID} > opentrafficshaper::plugins::statistics::STATISTICS_PERIOD) {
my $interface = getInterface($interfaceID);
$logger->log(LOG_INFO,"[TCSTATS] Generating stats for '%s'",$interfaceID);
# TC commands to run
my $cmd = [ '/sbin/tc', '-j', '-s', 'class', 'show', 'dev', $interface->{'Device'}, 'parent', '1:' ];
# Create task
my $task = POE::Wheel::Run->new(
Program => $cmd,
StdinFilter => POE::Filter::Line->new(),
StdoutFilter => opentrafficshaper::POE::Filter::TCStatistics->new(),
StderrFilter => POE::Filter::Line->new(),
StdoutEvent => '_task_child_stdout',
StderrEvent => '_task_child_stderr',
CloseEvent => '_task_child_close',
) or $logger->log(LOG_ERR,"[TCSTATS] TC: Unable to start task");
# Intercept SIGCHLD
$kernel->sig_child($task->ID, "_SIGCHLD");
# Wheel events include the wheel's ID.
$heap->{task_by_wid}->{$task->ID} = $task;
# Signal events include the process ID.
$heap->{task_by_pid}->{$task->PID} = $task;
# Signal events include the process ID.
$heap->{task_data}->{$task->ID} = {
'Timestamp' => $now,
'Interface' => $interfaceID,
'CurrentStat' => { }
};
# Build commandline string
my $cmdStr = join(' ',@{$cmd});
$logger->log(LOG_DEBUG,"[TCSTATS] TASK/%s: Starting '%s' as %s with PID %s",$task->ID,$cmdStr,$task->ID,$task->PID);
# Set last time we were run to now
$globals->{'LastStats'}->{$interfaceID} = $now;
# Grab next one for below calcs...
$interfaceID = shift(@interfaces);
}
# Set default tick period
my $tickPeriod = opentrafficshaper::plugins::statistics::STATISTICS_PERIOD;
# Calculate optimal wait time
if (defined($interfaceID)) {
$tickPeriod = opentrafficshaper::plugins::statistics::STATISTICS_PERIOD - ($now - $globals->{'LastStats'}->{$interfaceID});
}
# Make sure wait time is not too long
if ($tickPeriod > opentrafficshaper::plugins::statistics::STATISTICS_PERIOD) {
$tickPeriod = opentrafficshaper::plugins::statistics::STATISTICS_PERIOD;
}
# Make sure wait time is not too short
if ($tickPeriod < opentrafficshaper::plugins::statistics::STATISTICS_PERIOD) {
$tickPeriod = opentrafficshaper::plugins::statistics::STATISTICS_PERIOD;
}
$kernel->delay('_tick' => $tickPeriod);
};
# Child writes to STDOUT
sub _task_child_stdout
{
my ($kernel,$heap,$stat,$task_id) = @_[KERNEL,HEAP,ARG0,ARG1];
my $task = $heap->{task_by_wid}->{$task_id};
# Grab task data
my $taskData = $heap->{'task_data'}->{$task_id};
my $interface = $taskData->{'Interface'};
my $timestamp = $taskData->{'Timestamp'};
# Stats ID to update
my $sid;
# Default to transmit statistics
my $direction = opentrafficshaper::plugins::statistics::STATISTICS_DIR_TX;
# Is this a system class?
my $classChildDec = hex($stat->{'TCClassChild'});
# Check if this is a limit class...
if (opentrafficshaper::plugins::tc::isPoolTcClass($interface,$stat->{'TCClassParent'},$stat->{'TCClassChild'})) {
if (defined(my $pid = opentrafficshaper::plugins::tc::getPIDFromTcClass($interface,$stat->{'TCClassParent'},
$stat->{'TCClassChild'}))
) {
$sid = opentrafficshaper::plugins::statistics::setSIDFromPID($pid);
$direction = opentrafficshaper::plugins::statistics::getTrafficDirection($pid,$interface);
} else {
$logger->log(LOG_WARN,"[TCSTATS] Pool traffic class '%s:%s' NOT FOUND",$stat->{'TCClassParent'},
$stat->{'TCClassChild'}
);
}
} else {
# Class = 1 is the root
# XXX: Should this be hard coded or used like TC_ROOT_CLASS is
if ($classChildDec == 1) {
# This is a special case case
$sid = opentrafficshaper::plugins::statistics::setSIDFromCID($interface,0);
} else {
# Save the class with the decimal number
if (my $classID = opentrafficshaper::plugins::tc::getCIDFromTcClass($interface,
opentrafficshaper::plugins::tc::TC_ROOT_CLASS,$stat->{'TCClassChild'})
) {
$sid = opentrafficshaper::plugins::statistics::setSIDFromCID($interface,$classID);
} else {
$logger->log(LOG_WARN,"[TCSTATS] System traffic class '%s:%s' NOT FOUND",$stat->{'TCClassParent'},
$stat->{'TCClassChild'}
);
}
}
}
# Make sure we have the lid now
if (defined($sid)) {
# Build our submission
$stat->{'Timestamp'} = $timestamp;
$stat->{'Direction'} = $direction;
$taskData->{'Stats'}->{$sid} = $stat;
}
}
# Child writes to STDERR
sub _task_child_stderr
{
my ($kernel,$heap,$stdout,$task_id) = @_[KERNEL,HEAP,ARG0,ARG1];
my $task = $heap->{task_by_wid}->{$task_id};
$logger->log(LOG_WARN,"[TCSTATS] TASK/%s: STDERR => %s",$task_id,$stdout);
}
# Child closed its handles, it won't communicate with us, so remove it
sub _task_child_close
{
my ($kernel,$heap,$task_id) = @_[KERNEL,HEAP,ARG0];
my $task = $heap->{task_by_wid}->{$task_id};
my $taskData = $heap->{'task_data'}->{$task_id};
# May have been reaped by task_sigchld()
if (!defined($task)) {
$logger->log(LOG_DEBUG,"[TCSTATS] TASK/%s: Closed dead child",$task_id);
return;
}
# Push consolidated update through
$kernel->post("statistics" => "update" => $taskData->{'Stats'});
$logger->log(LOG_DEBUG,"[TCSTATS] TASK/%s: Closed PID %s",$task_id,$task->PID);
# Cleanup
delete($heap->{task_by_pid}->{$task->PID});
delete($heap->{task_by_wid}->{$task_id});
delete($heap->{task_data}->{$task_id});
}
# Reap the dead child
sub _task_handle_SIGCHLD
{
my ($kernel,$heap,$pid,$status) = @_[KERNEL,HEAP,ARG1,ARG2];
my $task = $heap->{task_by_pid}->{$pid};
$logger->log(LOG_DEBUG,"[TCSTATS] TASK: Task with PID %s exited with status %s",$pid,$status);
# May have been reaped by task_child_close()
return if (!defined($task));
# Cleanup
delete($heap->{task_by_pid}->{$pid});
delete($heap->{task_by_wid}->{$task->ID});
delete($heap->{task_data}->{$task->ID});
}
# Handle SIGINT
sub _task_handle_SIGINT
{
my ($kernel,$heap,$signal_name) = @_[KERNEL,HEAP,ARG0];
# Shutdown stdin on all children, this will terminate /sbin/tc
foreach my $task_id (keys %{$heap->{'task_by_wid'}}) {
my $task = $heap->{'task_by_wid'}{$task_id};
# $kernel->sig_child($task->PID, "asig_child");
# $task->kill("INT"); #NK: doesn't work
$kernel->post($task,"shutdown_stdin"); #NK: doesn't work
}
$logger->log(LOG_WARN,"[TCSTATS] Killed children processes");
}
1;
# vim: ts=4
Copyright (c) 2013, AllWorldIT
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
2013-06-22
Unzip v3.0.0 dist
rm -f ./css/bootstrap.css ./css/bootstrap-theme.css ./js/bootstrap.js
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.