33from uuid import UUID
44
55import openvpn_status
6+ import swapper
67from django .core .management import BaseCommand
78from Exscript .protocols import telnetlib
8- from netaddr import EUI , mac_unix
9+ from netaddr import EUI , AddrFormatError , mac_unix
910
1011from .... import settings as app_settings
12+ from ....base .models import sanitize_mac_address
1113from ....utils import load_model
1214
13- logger = logging .getLogger (__name__ )
14-
15- RE_VIRTUAL_ADDR_MAC = re .compile ("^{0}:{0}:{0}:{0}:{0}:{0}" .format ("[a-f0-9]{2}" ), re .I )
1615TELNET_CONNECTION_TIMEOUT = 30 # In seconds
1716
18-
1917RadiusAccounting = load_model ("RadiusAccounting" )
2018
19+ logger = logging .getLogger (__name__ )
20+
2121
2222class BaseConvertCalledStationIdCommand (BaseCommand ):
23+ logger = logger
24+
25+ def _search_mac_address (self , common_name ):
26+ """
27+ Search for a MAC address in the given common_name string.
28+ Supports colon-separated, dash-separated, dot-separated,
29+ and unseparated MAC formats.
30+ """
31+ mac_patterns = [
32+ r"([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}" ,
33+ r"([0-9A-Fa-f]{4}\.){2}[0-9A-Fa-f]{4}" ,
34+ r"(?<![0-9A-Fa-f])[0-9A-Fa-f]{12}(?![0-9A-Fa-f])" ,
35+ ]
36+ for pattern in mac_patterns :
37+ match = re .search (pattern , common_name )
38+ if match :
39+ return match [0 ]
40+ raise IndexError (f"No MAC address found in '{ common_name } '" )
41+
2342 help = "Correct Called Station IDs of Radius Sessions"
2443
2544 def _get_raw_management_info (self , host , port , password ):
@@ -42,29 +61,29 @@ def _get_openvpn_routing_info(self, host, port=7505, password=None):
4261 try :
4362 raw_info = self ._get_raw_management_info (host , port , password )
4463 except ConnectionRefusedError :
45- logger .warning (
64+ BaseConvertCalledStationIdCommand . logger .warning (
4665 "Unable to establish telnet connection to "
4766 f"{ host } on { port } . Skipping!"
4867 )
4968 return {}
5069 except (OSError , TimeoutError , EOFError ) as error :
51- logger .warning (
70+ BaseConvertCalledStationIdCommand . logger .warning (
5271 f"Error encountered while connecting to { host } :{ port } : { error } . "
5372 "Skipping!"
5473 )
5574 return {}
5675 except Exception :
57- logger .warning (
76+ BaseConvertCalledStationIdCommand . logger .warning (
5877 f"Error encountered while connecting to { host } :{ port } . Skipping!"
5978 )
6079 return {}
6180 try :
6281 parsed_info = openvpn_status .parse_status (raw_info )
6382 return parsed_info .routing_table
6483 except openvpn_status .ParsingError as error :
65- logger .warning (
84+ BaseConvertCalledStationIdCommand . logger .warning (
6685 "Unable to parse information received from "
67- f"{ host } :{ port } . ParsingError: { error } . Skipping!" ,
86+ f"{ host } :{ port } . ParsingError: { error } . Skipping!"
6887 )
6988 return {}
7089
@@ -74,7 +93,7 @@ def _get_radius_session(self, unique_id):
7493 unique_id = unique_id
7594 )
7695 except RadiusAccounting .DoesNotExist :
77- logger .warning (
96+ BaseConvertCalledStationIdCommand . logger .warning (
7897 f'RadiusAccount object with unique_id "{ unique_id } " does not exist.'
7998 )
8099
@@ -88,24 +107,11 @@ def _get_called_station_setting(self, radius_session):
88107 # but will removed in future versions
89108 return {org_id : app_settings .CALLED_STATION_IDS [organization .slug ]}
90109 except KeyError :
91- logger .error (
110+ BaseConvertCalledStationIdCommand . logger .error (
92111 "OPENWISP_RADIUS_CALLED_STATION_IDS does not contain setting "
93112 f'for "{ radius_session .organization .name } " organization'
94113 )
95114
96- def _get_unconverted_sessions (self , org , unconverted_ids ):
97- lookup = dict (
98- called_station_id__in = unconverted_ids ,
99- stop_time__isnull = True ,
100- )
101- try :
102- UUID (org )
103- except ValueError :
104- lookup ["organization__slug" ] = org
105- else :
106- lookup ["organization__id" ] = org
107- return RadiusAccounting .objects .filter (** lookup ).iterator ()
108-
109115 def add_arguments (self , parser ):
110116 parser .add_argument ("--unique_id" , action = "store" , type = str , default = "" )
111117
@@ -128,15 +134,24 @@ def handle(self, *args, **options):
128134 for org , config in called_station_id_setting .items ():
129135 routing_dict = {}
130136 for openvpn_config in config ["openvpn_config" ]:
131- routing_dict .update (
132- self ._get_openvpn_routing_info (
133- openvpn_config ["host" ],
134- openvpn_config .get ("port" , 7505 ),
135- openvpn_config .get ("password" , None ),
136- )
137+ raw_routing = self .__class__ ._get_openvpn_routing_info (
138+ self ,
139+ openvpn_config ["host" ],
140+ openvpn_config .get ("port" , 7505 ),
141+ openvpn_config .get ("password" , None ),
137142 )
143+ normalized_routing = {}
144+ for k , v in raw_routing .items ():
145+ try :
146+ norm_key = str (EUI (k , dialect = mac_unix )).lower ()
147+ except Exception :
148+ norm_key = k .lower ()
149+ normalized_routing [norm_key ] = v
150+ routing_dict .update (normalized_routing )
138151 if not routing_dict :
139- logger .info (f'No routing information found for "{ org } " organization' )
152+ BaseConvertCalledStationIdCommand .logger .info (
153+ f'No routing information found for "{ org } " organization'
154+ )
140155 continue
141156
142157 if unique_id :
@@ -145,24 +160,68 @@ def handle(self, *args, **options):
145160 qs = self ._get_unconverted_sessions (org , config ["unconverted_ids" ])
146161 for radius_session in qs :
147162 try :
148- common_name = routing_dict [
149- str (EUI (radius_session .calling_station_id , dialect = mac_unix ))
150- ].common_name
151- mac_address = RE_VIRTUAL_ADDR_MAC .search (common_name )[0 ]
152- from openwisp_radius .mac_utils import sanitize_mac_address
153- radius_session .called_station_id = sanitize_mac_address (mac_address )
154- except KeyError :
155- logger .warning (
156- "Failed to find routing information for "
163+ lookup_key = str (
164+ EUI (radius_session .calling_station_id , dialect = mac_unix )
165+ ).lower ()
166+ except (AddrFormatError , ValueError , TypeError ):
167+ BaseConvertCalledStationIdCommand .logger .warning (
168+ f"Invalid calling_station_id for session "
157169 f"{ radius_session .session_id } . Skipping!"
158170 )
171+ continue
172+ if lookup_key not in routing_dict :
173+
174+ def _strip_leading_zeros (k ):
175+ parts = k .split (":" )
176+ return ":" .join ([p .lstrip ("0" ) or "0" for p in parts ])
177+
178+ alt_key = _strip_leading_zeros (lookup_key )
179+ if alt_key in routing_dict :
180+ routing_dict [lookup_key ] = routing_dict [alt_key ]
181+ else :
182+ BaseConvertCalledStationIdCommand .logger .warning (
183+ "Failed to find routing information for "
184+ f"{ radius_session .session_id } . Skipping!"
185+ )
186+ continue
187+
188+ common_name = routing_dict [lookup_key ].common_name
189+
190+ try :
191+ mac_address = self ._search_mac_address (common_name )
159192 except (TypeError , IndexError ):
160- logger .warning (
193+ BaseConvertCalledStationIdCommand . logger .warning (
161194 f'Failed to find a MAC address in "{ common_name } ". '
162195 f"Skipping { radius_session .session_id } !"
163196 )
164- else :
165- radius_session .save ()
197+ continue
198+ radius_session .called_station_id = sanitize_mac_address (mac_address )
199+ radius_session .save ()
200+
201+ def _get_unconverted_sessions (self , org , unconverted_ids ):
202+ """
203+ Get unconverted sessions for the given organization and unconverted IDs.
204+ """
205+ normalized_ids = [sanitize_mac_address (uid ) for uid in unconverted_ids ]
206+ if isinstance (org , str ):
207+ try :
208+ org_uuid = UUID (org )
209+ except ValueError :
210+ Organization = swapper .load_model ("openwisp_users" , "Organization" )
211+ try :
212+ organization = Organization .objects .get (slug = org )
213+ org_uuid = organization .id
214+ except Organization .DoesNotExist :
215+ self .logger .warning (f"Organization '{ org } ' not found" )
216+ return RadiusAccounting .objects .none ()
217+ else :
218+ org_uuid = org .id
219+
220+ return RadiusAccounting .objects .filter (
221+ organization_id = org_uuid ,
222+ called_station_id__in = normalized_ids ,
223+ stop_time__isnull = True ,
224+ )
166225
167226
168227# monkey patching for openvpn_status begins
0 commit comments