From 742b50d21b8dc7669b5ec9556f556288c0705530 Mon Sep 17 00:00:00 2001
From: Nigel Kukard <nkukard@lbsd.net>
Date: Sat, 14 Sep 2013 17:51:51 +0000
Subject: [PATCH] Refactored code, added support for overrides

---
 opentrafficshaper/plugins/configmanager.pm | 729 +++++++++++++--------
 1 file changed, 467 insertions(+), 262 deletions(-)

diff --git a/opentrafficshaper/plugins/configmanager.pm b/opentrafficshaper/plugins/configmanager.pm
index eb92e76..5c5804d 100644
--- a/opentrafficshaper/plugins/configmanager.pm
+++ b/opentrafficshaper/plugins/configmanager.pm
@@ -43,12 +43,17 @@ our (@ISA,@EXPORT,@EXPORT_OK);
 		setLimitAttribute
 		getLimitAttribute
 
+		getOverride
+		getOverrides
+
 		getShaperState
 		setShaperState
 
 		getTrafficClasses
 
-		getPriorityName
+		getTrafficClassName
+
+		isTrafficClassValid
 );
 
 use constant {
@@ -63,42 +68,61 @@ use constant {
 };
 
 # Mandatory config attributes
-sub CONFIG_ATTRIBUTES {
+sub LIMIT_REQUIRED_ATTRIBUTES {
 	qw(
 		Username IP
 		GroupID ClassID
-		TrafficLimitTx TrafficLimitRx TrafficLimitTxBurst TrafficLimitRxBurst
+		TrafficLimitTx TrafficLimitRx
 		Expires Status
 		Source
 	)
 }
 
-# Changeset attributes - things that can be changed on the fly
-sub CHANGESET_ATTRIBUTES {
+# Limit Changeset attributes - things that can be changed on the fly in the shaper
+sub LIMIT_CHANGESET_ATTRIBUTES {
 	qw(
 		GroupID ClassID
 		TrafficLimitTx TrafficLimitRx TrafficLimitTxBurst TrafficLimitRxBurst
-		Expires
 	)
 }
 
 # Persistent attributes supported
-sub PERSISTENT_ATTRIBUTES {
+sub LIMIT_PERSISTENT_ATTRIBUTES {
 	qw(
 		Username IP
 		GroupID ClassID
-		TrafficLimitTx TrafficLimitRx TrafficLimitTxBurst TrafficLimitRxBurst
+		TrafficLimitTx TrafficLimitRx TrafficLimitTxBurst TrafficLimitRxBurst TrafficPriority
+		FriendlyName Notes
 		Expires Created
 		Source
 	)
 }
 
-# Override attributes supported
-sub OVERRIDE_ATTRIBUTES {
+# Mandatory override attribute, one is required
+sub OVERRIDE_REQUIRED_ATTRIBUTES {
 	qw(
+		Username IP
+		GroupID
+	)
+}
+# Override changeset attributes
+sub OVERRIDE_CHANGESET_ATTRIBUTES {
+	qw(
+		ClassID
+		TrafficLimitTx TrafficLimitRx TrafficLimitTxBurst TrafficLimitRxBurst
+	)
+}
+# Override attributes supported for persistent storage
+sub OVERRIDE_PERSISTENT_ATTRIBUTES {
+	qw(
+		Key
 		Username IP
 		GroupID ClassID
 		TrafficLimitTx TrafficLimitRx TrafficLimitTxBurst TrafficLimitRxBurst
+		FriendlyName Notes
+		Expires Created
+		Source
+		LastUpdate
 	)
 }
 
@@ -140,11 +164,76 @@ my $config = {
 # Pending changes
 my $changeQueue = { };
 
-# Main variables handling our limits
+#
+# LIMITS
+#
+# Supoprted user attributes:
+# * Username
+#    - Users username
+# * IP
+#    - Users IP
+# * GroupID
+#    - Group ID
+# * ClassID
+#    - Class ID
+# * TrafficLimitTx
+#    - Traffic limit in kbps
+# * TrafficLimitRx
+#    - Traffic limit in kbps
+# * TrafficLimitTxBurst
+#    - Traffic bursting limit in kbps
+# * TrafficLimitRxBurst
+#    - Traffic bursting limit in kbps
+# * 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
 my $limits = { };
 my $limitIPMap = { };
 my $limitIDMap = { };
 my $limitIDCounter = 1;
+
+#
+# OVERRIDES
+#
+# Selection criteria:
+# * Username
+#    - Users username
+# * IP
+#    - Users IP
+# * GroupID
+#    - Group ID
+#
+# Overrides:
+# * ClassID
+#    - Class ID
+# * TrafficLimitTx
+#    - Traffic limit in kbps
+# * TrafficLimitRx
+#    - Traffic limit in kbps
+# * TrafficLimitTxBurst
+#    - Traffic bursting limit in kbps
+# * TrafficLimitRxBurst
+#    - Traffic bursting limit in kbps
+#
+# Parameters:
+# * 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
 my $overrides = { };
 
 
@@ -246,7 +335,9 @@ sub plugin_init
 			_stop => \&session_stop,
 
 			tick => \&session_tick,
-			process_change => \&process_change,
+
+			process_limit_change => \&process_limit_change,
+			process_override_change => \&process_override_change,
 
 			handle_SIGHUP => \&handle_SIGHUP,
 		}
@@ -273,7 +364,7 @@ sub session_start
 
 	# Load config
 	if (-f $config->{'statefile'}) {
-		_load_statefile();
+		_load_statefile($kernel);
 	} else {
 		$logger->log(LOG_WARN,"[CONFIGMANAGER] Statefile '$config->{'statefile'}' cannot be opened: $!");
 	}
@@ -314,216 +405,30 @@ sub session_stop
 
 
 # Time ticker for processing changes
-sub session_tick {
+sub session_tick
+{
 	my $kernel = $_[KERNEL];
 
-
-	# Now
-	my $now = time();
-
-
-	#
-	# LOOP WITH CHANGES
-	#
-
-	foreach my $lid (keys %{$changeQueue}) {
-		# Changes for limit
-		# Minimum required info is:
-		# - Username
-		# - IP
-		# - Status
-		# - LastUpdate
-		my $climit = $changeQueue->{$lid};
-
-		#
-		# LIMIT IN LIST
-		#
-		if (defined(my $glimit = $limits->{$lid})) {
-
-			# This is a new limit notification
-			if ($climit->{'Status'} eq "new") {
-				$logger->log(LOG_INFO,"[CONFIGMANAGER] Limit '$climit->{'Username'}' [$lid], limit already live but new state provided?");
-
-				# Get the changes we made and push them to the shaper
-				if (my $changes = _getChangeset($glimit,$climit)) {
-					# Post to shaper
-					$kernel->post("shaper" => "change" => $lid => $changes);
-				}
-
-				# Remove from change queue
-				delete($changeQueue->{$lid});
-
-			# Online or "ping" status notification
-			} elsif ($climit->{'Status'} eq "online") {
-				$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Limit '$climit->{'Username'}' [$lid], limit still online");
-
-				# Get the changes we made and push them to the shaper
-				if (my $changes = _getChangeset($glimit,$climit)) {
-					# Post to shaper
-					$kernel->post("shaper" => "change" => $lid => $changes);
-				}
-
-				# Remove from change queue
-				delete($changeQueue->{$lid});
-
-			# Offline notification, this we going to treat specially
-			} elsif ($climit->{'Status'} eq "offline") {
-
-				# We first check if this update was received some time ago, and if it exceeds our expire time
-				# We don't want to immediately remove a limit, only for him to come back on a few seconds later, the cost in exec()'s
-				# would be pretty high
-				if ($now - $climit->{'LastUpdate'} > TIMEOUT_EXPIRE_OFFLINE) {
-
-					# Remove entry if no longer live
-					if ($glimit->{'_shaper.state'} == SHAPER_NOTLIVE) {
-						$logger->log(LOG_INFO,"[CONFIGMANAGER] Limit '$climit->{'Username'}' [$lid] offline and removed from shaper");
-
-						# Remove from system
-						delete($limits->{$lid});
-						# Remove from change queue
-						delete($changeQueue->{$lid});
-						# Set this UID as no longer using this IP
-						# NK: If we try remove it before the limit is actually removed we could get a reconnection causing this value
-						#     to be totally gone, which means we not tracking this limit using this IP anymore, not easily solved!!
-						delete($limitIPMap->{$glimit->{'IP'}}->{$lid});
-						# Check if we can delete the IP too
-						if (keys %{$limitIPMap->{$glimit->{'IP'}}} == 0) {
-							delete($limitIPMap->{$glimit->{'IP'}});
-						}
-
-						# Next record, we don't want to do any updates below
-						next;
-
-					# Push to shaper
-					} elsif ($glimit->{'_shaper.state'} == SHAPER_LIVE) {
-						$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Limit '$climit->{'Username'}' [$lid] offline, queue remove from shaper");
-
-						# Post removal to shaper
-						$kernel->post("shaper" => "remove" => $lid);
-
-					} else {
-						$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Limit '$climit->{'Username'}' [$lid], limit in list, but offline now and".
-								" expired, still live, waiting for shaper");
-					}
-				}
-			}
-
-			# Update the limit data
-			$glimit->{'Status'} = $climit->{'Status'};
-			$glimit->{'LastUpdate'} = $climit->{'LastUpdate'};
-			$glimit->{'Expires'} = $climit->{'Expires'};
-
-		#
-		# LIMIT NOT IN LIST
-		#
-		} else {
-			# We take new and online notifications the same way here if the limit is not in our global limit list already
-		   if (($climit->{'Status'} eq "new" || $climit->{'Status'} eq "online")) {
-				$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Processing new limit '$climit->{'Username'}' [$lid]");
-
-				# We first going to look for IP conflicts...
-				my @ipLimits = keys %{$limitIPMap->{$climit->{'IP'}}};
-				if (
-					# If there is already an entry and its not us ...
-					( @ipLimits == 1 && !defined($limitIPMap->{$climit->{'IP'}}->{$lid}) )
-					# Or if there is more than 1 entry...
-					|| @ipLimits > 1
-				) {
-					# We not going to post this to the shaper, but we are going to override the status
-					$climit->{'Status'} = 'conflict';
-					$climit->{'_shaper.state'} = SHAPER_NOTLIVE;
-					# Give a bit of info
-					my @conflictUsernames;
-					foreach my $lid (@ipLimits) {
-						push(@conflictUsernames,$limits->{$lid}->{'Username'});
-					}
-					# Output log line
-					$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process limit '".$climit->{'Username'}."' IP '$climit->{'IP'}' conflicts with users '".
-							join(',',@conflictUsernames)."'.");
-
-					# We cannot trust shaping when there is more than 1 limit on the IP, so we going to remove all limits with this
-					# IP from the shaper below...
-					foreach my $lid2 (@ipLimits) {
-						# Check if the limit has been setup already (all but the limit we busy with, as its setup below)
-						if (defined($limitIPMap->{$climit->{'IP'}}->{$lid2})) {
-							my $glimit2 = $limits->{$lid2};
-
-							# If the limit is active or pending on the shaper, remove it
-							if ($glimit2->{'_shaper.state'} == SHAPER_LIVE || $glimit2->{'_shaper.state'} == SHAPER_PENDING) {
-								$logger->log(LOG_WARN,"[CONFIGMANAGER] Removing conflicted limit '".$glimit2->{'Username'}."' [$lid2] from shaper'");
-								# Post removal to shaper
-								$kernel->post("shaper" => "remove" => $lid2);
-								# Update that we're offline directly to global limit table
-								$glimit2->{'Status'} = 'conflict';
-							}
-						}
-					}
-
-				# All looks good, no conflicts, we're set to add this limit!
-				} else {
-					# Post to the limit to the shaper
-					$climit->{'_shaper.state'} = SHAPER_PENDING;
-					$kernel->post("shaper" => "add" => $lid);
-
-				}
-
-				# Set this UID as using this IP
-				$limitIPMap->{$climit->{'IP'}}->{$lid} = 1;
-
-				# This is now live
-				$limits->{$lid} = $climit;
-
-			# Limit is not in our list and this is an unknown state we're trasitioning to
-			} else {
-				$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Ignoring limit '$climit->{'Username'}' [$lid] state '$climit->{'Status'}', not in our".
-						" global list");
-			}
-
-			# Remove from change queue
-			delete($changeQueue->{$lid});
-		}
-
-	}
-
-
-	#
-	# CHECK OUT CONNECTED LIMITS
-	#
-	foreach my $lid (keys %{$limits}) {
-		# Global limit
-		my $glimit = $limits->{$lid};
-
-
-if (!defined($glimit->{'Expires'})) {
-	use Data::Dumper; warn "UNDEFINED: ".Dumper($glimit,$lid);
-}
-
-		# Check for expired limits
-		if ($glimit->{'Expires'} && $glimit->{'Expires'} < $now) {
-			$logger->log(LOG_INFO,"[CONFIGMANAGER] Limit '$glimit->{'Username'}' has expired, marking offline");
-			# Looks like this limit has expired?
-			my $climit = {
-				'Username' => $glimit->{'Username'},
-				'IP' => $glimit->{'IP'},
-				'Status' => 'offline',
-				'LastUpdate' => $glimit->{'LastUpdate'},
-			};
-			# Add to change queue
-			$changeQueue->{$lid} = $climit;
-		}
-	}
+	_process_limit_change_queue($kernel);
 
 	# Reset tick
 	$kernel->delay(tick => TICK_PERIOD);
-};
-
+}
 
-# Process shaper change
-sub process_change
+# Process limit change
+sub process_limit_change
 {
 	my ($kernel, $limit) = @_[KERNEL, ARG0];
 
-	_process_change($limit);
+	_process_limit_change($limit);
+}
+
+# Process override change
+sub process_override_change
+{
+	my ($kernel, $override) = @_[KERNEL, ARG0];
+
+	_process_override_change($override);
 }
 
 
@@ -590,6 +495,7 @@ sub getLimits
 	return (keys %{$limits});
 }
 
+
 # Function to set a limit attribute
 sub setLimitAttribute
 {
@@ -618,6 +524,26 @@ sub getLimitAttribute
 }
 
 
+# Function to return a override
+sub getOverride
+{
+	my $oid = shift;
+
+	if (defined($overrides->{$oid})) {
+		my %override = %{$overrides->{$oid}};
+		return \%override;
+	}
+	return;
+}
+
+
+# Function to return a list of override ID's
+sub getOverrides
+{
+	return (keys %{$overrides});
+}
+
+
 # Function to set shaper state on a limit
 sub setShaperState
 {
@@ -649,11 +575,22 @@ sub getTrafficClasses
 }
 
 
-# Function to get priority name
-sub getPriorityName
+# Function to get class name
+sub getTrafficClassName
 {
-	my $prio = shift;
-	return $config->{'classes'}->{$prio};
+	my $class = shift;
+	return $config->{'classes'}->{$class};
+}
+
+
+# Function to check if traffic class is valid
+sub isTrafficClassValid
+{
+	my $class = shift;
+	if (defined($config->{'classes'}->{$class})) {
+		return $class;
+	}
+	return;
 }
 
 
@@ -672,14 +609,14 @@ sub handle_SIGHUP
 #
 
 # Function to compute the changes between two users
-sub _getChangeset
+sub _getLimitChangeset
 {
 	my ($orig,$new) = @_;
 
 	my $res;
 
 	# Loop through what can change
-	foreach my $item (CHANGESET_ATTRIBUTES) {
+	foreach my $item (LIMIT_CHANGESET_ATTRIBUTES) {
 		# Check if its first set, if it is, check if its changed
 		if (defined($new->{$item}) && $orig->{$item} ne $new->{$item}) {
 			# If so record it & make the change
@@ -692,53 +629,39 @@ sub _getChangeset
 
 
 # This is the real function
-# Supoprted user attributes:
-#
-# Username
-#  - Users username
-# IP
-#  - Users IP
-# GroupID
-#  - Group ID
-# ClassID
-#  - Class ID
-# TrafficLimitTx
-#  - Traffic limit in kbps
-# TrafficLimitRx
-#  - Traffic limit in kbps
-# TrafficLimitTxBurst
-#  - Traffic bursting limit in kbps
-# TrafficLimitRxBurst
-#  - Traffic bursting limit in kbps
-# Expires
-#  - Unix timestamp when this entry expires, 0 if never
-# Status
-#  - new
-#  - offline
-#  - online
-#  - unknown
-# Source
-#  - This is the source of the limit, typically  plugin.ModuleName
-sub _process_change
+sub _process_limit_change
 {
 	my $limit = shift;
 
 
+	# Check if we have all the attributes we need
+	my $isInvalid;
+	foreach my $attr (LIMIT_REQUIRED_ATTRIBUTES) {
+		if (!defined($limit->{$attr})) {
+			$isInvalid = $attr;
+			last;
+		}
+	}
+	if ($isInvalid) {
+		$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process limit change as not attributes is missing: '$isInvalid'");
+		return;
+	}
+
 	# We start off blank so we only pull in whats supported
 	my $limitChange;
-	if (!($limitChange->{'Username'} = $limit->{'Username'})) {
+	if (!defined($limitChange->{'Username'} = $limit->{'Username'})) {
 		$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Cannot process limit change as username is invalid.");
 		return;
 	}
 	$limitChange->{'Username'} = $limit->{'Username'};
 	$limitChange->{'IP'} = $limit->{'IP'};
 	# Check group is OK
-	if (!($limitChange->{'GroupID'} = checkGroupID($limit->{'GroupID'}))) {
+	if (!defined($limitChange->{'GroupID'} = checkGroupID($limit->{'GroupID'}))) {
 		$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Cannot process limit change for '".$limit->{'Username'}."' as the GroupID is invalid.");
 		return;
 	}
 	# Check class is OK
-	if (!($limitChange->{'ClassID'} = checkClassID($limit->{'ClassID'}))) {
+	if (!defined($limitChange->{'ClassID'} = checkClassID($limit->{'ClassID'}))) {
 		$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Cannot process limit change for '".$limit->{'Username'}."' as the ClassID is invalid.");
 		return;
 	}
@@ -765,6 +688,10 @@ sub _process_change
 	# Set when this entry expires
 	$limitChange->{'Expires'} = defined($limit->{'Expires'}) ? $limit->{'Expires'} : 0;
 
+	# Set friendly name and notes
+	$limitChange->{'FriendlyName'} = $limit->{'FriendlyName'};
+	$limitChange->{'Notes'} = $limit->{'Notes'};
+
 	# Set when this entry was created
 	$limitChange->{'Created'} = defined($limit->{'Created'}) ? $limit->{'Created'} : time();
 
@@ -788,15 +715,82 @@ sub _process_change
 	# Set the user ID before we post to the change queue
 	$limitChange->{'ID'} = $lid;
 	$limitChange->{'LastUpdate'} = time();
-
 	# Push change to change queue
 	$changeQueue->{$lid} = $limitChange;
 }
 
 
+# This is the real process_override_change function
+sub _process_override_change
+{
+	my $override = shift;
+
+
+	# Pull in mandatory items and check if the result is valid
+	my $overrideChange;
+	my $isValid = 0;
+	foreach my $item (OVERRIDE_REQUIRED_ATTRIBUTES) {
+		$overrideChange->{$item} = $override->{$item};
+		$isValid++;
+	}
+	# Make sure we have at least 1
+	if (!$isValid) {
+		$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process override as there is no selection attribute");
+		return;
+	}
+
+	# Pull in attributes that can be changed
+	foreach my $item (OVERRIDE_CHANGESET_ATTRIBUTES) {
+		$overrideChange->{$item} = $override->{$item};
+	}
+
+	# Check group is OK
+	if (defined($overrideChange->{'GroupID'}) && !checkGroupID($overrideChange->{'GroupID'})) {
+		$logger->log(LOG_DEBUG,'[CONFIGMANAGER] Cannot process override for "User: %s, IP: %s, GroupID: %s" as the GroupID is invalid.',
+			prettyUndef($overrideChange->{'Username'}),prettyUndef($overrideChange->{'IP'}),prettyUndef($overrideChange->{'GroupID'})
+		);
+		return;
+	}
+
+	# Check class is OK
+	if (defined($overrideChange->{'ClassID'}) && !checkClassID($overrideChange->{'ClassID'})) {
+		$logger->log(LOG_DEBUG,'[CONFIGMANAGER] Cannot process override for "User: %s, IP: %s, GroupID: %s" as the ClassID is invalid.',
+			prettyUndef($overrideChange->{'Username'}),prettyUndef($overrideChange->{'IP'}),prettyUndef($overrideChange->{'GroupID'})
+		);
+		return;
+	}
+
+	# Set when this entry expires
+	$overrideChange->{'Expires'} = defined($override->{'Expires'}) ? $override->{'Expires'} : 0;
+
+	# Set friendly name and notes
+	$overrideChange->{'FriendlyName'} = $override->{'FriendlyName'};
+	$overrideChange->{'Notes'} = $override->{'Notes'};
+
+	# Set when this entry was created
+	$overrideChange->{'Created'} = defined($override->{'Created'}) ? $override->{'Created'} : time();
+
+	$overrideChange->{'LastUpdate'} = time();
+
+	# This is our key for this entry
+	my $oid = sprintf('%s%%%s%%%s',
+			defined($overrideChange->{'Username'}) ? $overrideChange->{'Username'} : "",
+			defined($overrideChange->{'IP'}) ? $overrideChange->{'IP'} : "",
+			defined($overrideChange->{'GroupID'}) ? $overrideChange->{'GroupID'} : ""
+	);
+	# Set the user ID before we post to the change queue
+	$overrideChange->{'Key'} = $oid;
+
+	$overrides->{$oid} = $overrideChange;
+}
+
+
 # Load our statefile
 sub _load_statefile
 {
+	my $kernel = shift;
+
+
 	# Check if the state file exists first of all
 	if (! -e $config->{'statefile'}) {
 		$logger->log(LOG_INFO,"[CONFIGMANAGER] Statefile '".$config->{'statefile'}."' doesn't exist");
@@ -822,19 +816,19 @@ sub _load_statefile
 
 		# Our user override
 		my $ouser;
-		foreach my $attr (OVERRIDE_ATTRIBUTES) {
+		foreach my $attr (OVERRIDE_PERSISTENT_ATTRIBUTES) {
 			if (defined($override->{$attr})) {
 				$ouser->{$attr} = $override->{$attr};
 			}
 		}
 
-		# Check username & IP are defined
-		if (!defined($ouser->{'Username'})) {
-			$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to load user override with no username '$section'");
+		# Check username, IP or gorup ID is defined
+		if (!defined($ouser->{'Key'})) {
+			$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to load override with no Key '$section'");
 			next;
 		}
 
-		$overrides->{$ouser->{'Username'}} = $ouser;
+		$overrides->{$ouser->{'Key'}} = $ouser;
 	}
 
 	# Loop with persistent users
@@ -843,7 +837,7 @@ sub _load_statefile
 
 		# User to push through to process change
 		my $cuser;
-		foreach my $attr (PERSISTENT_ATTRIBUTES) {
+		foreach my $attr (LIMIT_PERSISTENT_ATTRIBUTES) {
 			if (defined($user->{$attr})) {
 				$cuser->{$attr} = $user->{$attr};
 			}
@@ -853,12 +847,12 @@ sub _load_statefile
 
 		# Check username & IP are defined
 		if (!defined($cuser->{'Username'}) || !defined($cuser->{'IP'})) {
-			$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to load persistent user with no username or no IP '$section'");
+			$logger->log(LOG_WARN,"[CONFIGMANAGER] Failed to load persistent user with no username or IP '$section'");
 			next;
 		}
 
 		# Process this user
-		_process_change($cuser);
+		_process_limit_change($cuser);
 	}
 }
 
@@ -872,6 +866,12 @@ sub _write_statefile
 		return;
 	}
 
+	# Only write out if we actually have users, else we may of crashed?
+	if (keys %{$limits} < 1) {
+		$logger->log(LOG_WARN,"[CONFIGMANAGER] Not writing state file as there are no active users");
+		return;
+	}
+
 	# Create new state file object
 	my $state = new Config::IniFiles();
 
@@ -886,7 +886,7 @@ sub _write_statefile
 		# Add a new section for this user
 		$state->AddSection($section);
 		# Items we want for persistent entries
-		foreach my $pItem (PERSISTENT_ATTRIBUTES) {
+		foreach my $pItem (LIMIT_PERSISTENT_ATTRIBUTES) {
 			# Set items up
 			if (defined(my $value = $limits->{$lid}->{$pItem})) {
 				$state->newval($section,$pItem,$value);
@@ -902,7 +902,7 @@ sub _write_statefile
 		# Add a new section for this user
 		$state->AddSection($section);
 		# Items we want for override entries
-		foreach my $pItem (OVERRIDE_ATTRIBUTES) {
+		foreach my $pItem (OVERRIDE_PERSISTENT_ATTRIBUTES) {
 			# Set items up
 			if (defined(my $value = $overrides->{$username}->{$pItem})) {
 				$state->newval($section,$pItem,$value);
@@ -921,5 +921,210 @@ sub _write_statefile
 }
 
 
+# Do the actual queue processing
+sub _process_limit_change_queue
+{
+	my $kernel = shift;
+
+
+	# Now
+	my $now = time();
+
+	#
+	# LOOP WITH CHANGES
+	#
+
+	foreach my $lid (keys %{$changeQueue}) {
+		# Changes for limit
+		# Minimum required info is:
+		# - Username
+		# - IP
+		# - Status
+		# - LastUpdate
+		my $climit = $changeQueue->{$lid};
+
+		#
+		# LIMIT IN LIST
+		#
+		if (defined(my $glimit = $limits->{$lid})) {
+
+			# This is a new limit notification
+			if ($climit->{'Status'} eq "new") {
+				$logger->log(LOG_INFO,"[CONFIGMANAGER] Limit '$climit->{'Username'}' [$lid], limit already live but new state provided?");
+
+				# Get the changes we made and push them to the shaper
+				if (my $changes = _getLimitChangeset($glimit,$climit)) {
+					# Post to shaper
+					$kernel->post("shaper" => "change" => $lid => $changes);
+				}
+
+				# Remove from change queue
+				delete($changeQueue->{$lid});
+
+			# Online or "ping" status notification
+			} elsif ($climit->{'Status'} eq "online") {
+				$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Limit '$climit->{'Username'}' [$lid], limit still online");
+
+				# Get the changes we made and push them to the shaper
+				if (my $changes = _getLimitChangeset($glimit,$climit)) {
+					# Post to shaper
+					$kernel->post("shaper" => "change" => $lid => $changes);
+				}
+
+				# Remove from change queue
+				delete($changeQueue->{$lid});
+
+			# Offline notification, this we going to treat specially
+			} elsif ($climit->{'Status'} eq "offline") {
+
+				# We first check if this update was received some time ago, and if it exceeds our expire time
+				# We don't want to immediately remove a limit, only for him to come back on a few seconds later, the cost in exec()'s
+				# would be pretty high
+				if ($now - $climit->{'LastUpdate'} > TIMEOUT_EXPIRE_OFFLINE) {
+
+					# Remove entry if no longer live
+					if ($glimit->{'_shaper.state'} == SHAPER_NOTLIVE) {
+						$logger->log(LOG_INFO,"[CONFIGMANAGER] Limit '$climit->{'Username'}' [$lid] offline and removed from shaper");
+
+						# Remove from system
+						delete($limits->{$lid});
+						# Remove from change queue
+						delete($changeQueue->{$lid});
+						# Set this UID as no longer using this IP
+						# NK: If we try remove it before the limit is actually removed we could get a reconnection causing this value
+						#     to be totally gone, which means we not tracking this limit using this IP anymore, not easily solved!!
+						delete($limitIPMap->{$glimit->{'IP'}}->{$lid});
+						# Check if we can delete the IP too
+						if (keys %{$limitIPMap->{$glimit->{'IP'}}} == 0) {
+							delete($limitIPMap->{$glimit->{'IP'}});
+						}
+
+						# Next record, we don't want to do any updates below
+						next;
+
+					# Push to shaper
+					} elsif ($glimit->{'_shaper.state'} == SHAPER_LIVE) {
+						$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Limit '$climit->{'Username'}' [$lid] offline, queue remove from shaper");
+
+						# Post removal to shaper
+						$kernel->post("shaper" => "remove" => $lid);
+
+					} else {
+						$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Limit '$climit->{'Username'}' [$lid], limit in list, but offline now and".
+								" expired, still live, waiting for shaper");
+					}
+				}
+			}
+
+			# Update the limit data
+			$glimit->{'Status'} = $climit->{'Status'};
+			$glimit->{'LastUpdate'} = $climit->{'LastUpdate'};
+			$glimit->{'Expires'} = $climit->{'Expires'};
+
+			# Set these if they exist
+			if (defined($climit->{'FriendlyName'})) {
+				$glimit->{'FriendlyName'} = $climit->{'FriendlyName'};
+			}
+			if (defined($climit->{'Notes'})) {
+				$glimit->{'Notes'} = $climit->{'Notes'};
+			}
+
+		#
+		# LIMIT NOT IN LIST
+		#
+		} else {
+			# We take new and online notifications the same way here if the limit is not in our global limit list already
+		   if (($climit->{'Status'} eq "new" || $climit->{'Status'} eq "online")) {
+				$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Processing new limit '$climit->{'Username'}' [$lid]");
+
+				# We first going to look for IP conflicts...
+				my @ipLimits = keys %{$limitIPMap->{$climit->{'IP'}}};
+				if (
+					# If there is already an entry and its not us ...
+					( @ipLimits == 1 && !defined($limitIPMap->{$climit->{'IP'}}->{$lid}) )
+					# Or if there is more than 1 entry...
+					|| @ipLimits > 1
+				) {
+					# We not going to post this to the shaper, but we are going to override the status
+					$climit->{'Status'} = 'conflict';
+					$climit->{'_shaper.state'} = SHAPER_NOTLIVE;
+					# Give a bit of info
+					my @conflictUsernames;
+					foreach my $lid (@ipLimits) {
+						push(@conflictUsernames,$limits->{$lid}->{'Username'});
+					}
+					# Output log line
+					$logger->log(LOG_WARN,"[CONFIGMANAGER] Cannot process limit '".$climit->{'Username'}."' IP '$climit->{'IP'}' conflicts with users '".
+							join(',',@conflictUsernames)."'.");
+
+					# We cannot trust shaping when there is more than 1 limit on the IP, so we going to remove all limits with this
+					# IP from the shaper below...
+					foreach my $lid2 (@ipLimits) {
+						# Check if the limit has been setup already (all but the limit we busy with, as its setup below)
+						if (defined($limitIPMap->{$climit->{'IP'}}->{$lid2})) {
+							my $glimit2 = $limits->{$lid2};
+
+							# If the limit is active or pending on the shaper, remove it
+							if ($glimit2->{'_shaper.state'} == SHAPER_LIVE || $glimit2->{'_shaper.state'} == SHAPER_PENDING) {
+								$logger->log(LOG_WARN,"[CONFIGMANAGER] Removing conflicted limit '".$glimit2->{'Username'}."' [$lid2] from shaper'");
+								# Post removal to shaper
+								$kernel->post("shaper" => "remove" => $lid2);
+								# Update that we're offline directly to global limit table
+								$glimit2->{'Status'} = 'conflict';
+							}
+						}
+					}
+
+				# All looks good, no conflicts, we're set to add this limit!
+				} else {
+					# Post to the limit to the shaper
+					$climit->{'_shaper.state'} = SHAPER_PENDING;
+					$kernel->post("shaper" => "add" => $lid);
+
+				}
+
+				# Set this UID as using this IP
+				$limitIPMap->{$climit->{'IP'}}->{$lid} = 1;
+
+				# This is now live
+				$limits->{$lid} = $climit;
+
+			# Limit is not in our list and this is an unknown state we're trasitioning to
+			} else {
+				$logger->log(LOG_DEBUG,"[CONFIGMANAGER] Ignoring limit '$climit->{'Username'}' [$lid] state '$climit->{'Status'}', not in our".
+						" global list");
+			}
+
+			# Remove from change queue
+			delete($changeQueue->{$lid});
+		}
+
+	}
+
+
+	#
+	# CHECK OUT CONNECTED LIMITS
+	#
+	foreach my $lid (keys %{$limits}) {
+		# Global limit
+		my $glimit = $limits->{$lid};
+
+		# Check for expired limits
+		if ($glimit->{'Expires'} && $glimit->{'Expires'} < $now) {
+			$logger->log(LOG_INFO,"[CONFIGMANAGER] Limit '$glimit->{'Username'}' has expired, marking offline");
+			# Looks like this limit has expired?
+			my $climit = {
+				'Username' => $glimit->{'Username'},
+				'IP' => $glimit->{'IP'},
+				'Status' => 'offline',
+				'LastUpdate' => $glimit->{'LastUpdate'},
+			};
+			# Add to change queue
+			$changeQueue->{$lid} = $climit;
+		}
+	}
+}
+
+
 1;
 # vim: ts=4
-- 
GitLab