11import logging
22import re
3- from uuid import UUID
43
54import openvpn_status
5+ import swapper
66from django .core .management import BaseCommand
77from Exscript .protocols import telnetlib
88from netaddr import EUI , mac_unix
99
1010from .... import settings as app_settings
1111from ....utils import load_model
1212
13- logger = logging .getLogger (__name__ )
14-
1513RE_VIRTUAL_ADDR_MAC = re .compile ("^{0}:{0}:{0}:{0}:{0}:{0}" .format ("[a-f0-9]{2}" ), re .I )
1614TELNET_CONNECTION_TIMEOUT = 30 # In seconds
1715
18-
1916RadiusAccounting = load_model ("RadiusAccounting" )
2017
2118
19+ logger = logging .getLogger (__name__ )
20+
21+
2222class BaseConvertCalledStationIdCommand (BaseCommand ):
23+ logger = logger
24+
25+ def _search_mac_address (self , common_name ):
26+ match = RE_VIRTUAL_ADDR_MAC .search (common_name )
27+ if not match :
28+ raise IndexError (f"No MAC address found in '{ common_name } '" )
29+ return match [0 ]
30+
2331 help = "Correct Called Station IDs of Radius Sessions"
2432
2533 def _get_raw_management_info (self , host , port , password ):
@@ -42,29 +50,29 @@ def _get_openvpn_routing_info(self, host, port=7505, password=None):
4250 try :
4351 raw_info = self ._get_raw_management_info (host , port , password )
4452 except ConnectionRefusedError :
45- logger .warning (
53+ BaseConvertCalledStationIdCommand . logger .warning (
4654 "Unable to establish telnet connection to "
4755 f"{ host } on { port } . Skipping!"
4856 )
4957 return {}
5058 except (OSError , TimeoutError , EOFError ) as error :
51- logger .warning (
59+ BaseConvertCalledStationIdCommand . logger .warning (
5260 f"Error encountered while connecting to { host } :{ port } : { error } . "
5361 "Skipping!"
5462 )
5563 return {}
5664 except Exception :
57- logger .warning (
65+ BaseConvertCalledStationIdCommand . logger .warning (
5866 f"Error encountered while connecting to { host } :{ port } . Skipping!"
5967 )
6068 return {}
6169 try :
6270 parsed_info = openvpn_status .parse_status (raw_info )
6371 return parsed_info .routing_table
6472 except openvpn_status .ParsingError as error :
65- logger .warning (
73+ BaseConvertCalledStationIdCommand . logger .warning (
6674 "Unable to parse information received from "
67- f"{ host } :{ port } . ParsingError: { error } . Skipping!" ,
75+ f"{ host } :{ port } . ParsingError: { error } . Skipping!"
6876 )
6977 return {}
7078
@@ -74,7 +82,7 @@ def _get_radius_session(self, unique_id):
7482 unique_id = unique_id
7583 )
7684 except RadiusAccounting .DoesNotExist :
77- logger .warning (
85+ BaseConvertCalledStationIdCommand . logger .warning (
7886 f'RadiusAccount object with unique_id "{ unique_id } " does not exist.'
7987 )
8088
@@ -88,24 +96,11 @@ def _get_called_station_setting(self, radius_session):
8896 # but will removed in future versions
8997 return {org_id : app_settings .CALLED_STATION_IDS [organization .slug ]}
9098 except KeyError :
91- logger .error (
99+ BaseConvertCalledStationIdCommand . logger .error (
92100 "OPENWISP_RADIUS_CALLED_STATION_IDS does not contain setting "
93101 f'for "{ radius_session .organization .name } " organization'
94102 )
95103
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-
109104 def add_arguments (self , parser ):
110105 parser .add_argument ("--unique_id" , action = "store" , type = str , default = "" )
111106
@@ -128,41 +123,101 @@ def handle(self, *args, **options):
128123 for org , config in called_station_id_setting .items ():
129124 routing_dict = {}
130125 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- )
126+ raw_routing = self .__class__ ._get_openvpn_routing_info (
127+ self ,
128+ openvpn_config ["host" ],
129+ openvpn_config .get ("port" , 7505 ),
130+ openvpn_config .get ("password" , None ),
137131 )
132+ normalized_routing = {}
133+ for k , v in raw_routing .items ():
134+ try :
135+ norm_key = str (EUI (k , dialect = mac_unix )).lower ()
136+ except Exception :
137+ norm_key = k .lower ()
138+ normalized_routing [norm_key ] = v
139+ routing_dict .update (normalized_routing )
138140 if not routing_dict :
139- logger .info (f'No routing information found for "{ org } " organization' )
141+ BaseConvertCalledStationIdCommand .logger .info (
142+ f'No routing information found for "{ org } " organization'
143+ )
140144 continue
141145
142146 if unique_id :
143147 qs = [input_radius_session ]
144148 else :
145149 qs = self ._get_unconverted_sessions (org , config ["unconverted_ids" ])
146150 for radius_session in qs :
147- 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 (
151+ lookup_key = str (
152+ EUI (radius_session .calling_station_id , dialect = mac_unix )
153+ ).lower ()
154+ # If routing information doesn't contain the expected key,
155+ # try a tolerant fallback that strips leading zeros from each
156+ # octet (handles representations like '0b' vs 'b'). Only if
157+ # no variant is found, log a warning and skip this session.
158+ if lookup_key not in routing_dict :
159+
160+ def _strip_leading_zeros (k ):
161+ parts = k .split (":" )
162+ return ":" .join ([p .lstrip ("0" ) or "0" for p in parts ])
163+
164+ alt_key = _strip_leading_zeros (lookup_key )
165+ if alt_key in routing_dict :
166+ # use the alt_key mapping
167+ routing_dict [lookup_key ] = routing_dict [alt_key ]
168+ else :
169+ pass
170+ # If routing information doesn't contain the expected key,
171+ # log a warning and skip this session.
172+ BaseConvertCalledStationIdCommand .logger .warning (
156173 "Failed to find routing information for "
157174 f"{ radius_session .session_id } . Skipping!"
158175 )
176+ continue
177+
178+ common_name = routing_dict [lookup_key ].common_name
179+
180+ try :
181+ mac_address = self ._search_mac_address (common_name )
159182 except (TypeError , IndexError ):
160- logger .warning (
183+ BaseConvertCalledStationIdCommand . logger .warning (
161184 f'Failed to find a MAC address in "{ common_name } ". '
162185 f"Skipping { radius_session .session_id } !"
163186 )
164- else :
165- radius_session .save ()
187+ continue
188+
189+ from openwisp_radius .base .models import sanitize_mac_address
190+
191+ radius_session .called_station_id = sanitize_mac_address (mac_address )
192+ radius_session .save ()
193+
194+ def _get_unconverted_sessions (self , org , unconverted_ids ):
195+ """
196+ Get unconverted sessions for the given organization and unconverted IDs.
197+ """
198+ from uuid import UUID
199+
200+ # org might be a string UUID or slug from settings
201+ if isinstance (org , str ):
202+ try :
203+ # Try to parse as UUID first
204+ org_uuid = UUID (org )
205+ except ValueError :
206+ # If not a UUID, treat as slug and look up the organization
207+ Organization = swapper .load_model ("openwisp_users" , "Organization" )
208+ try :
209+ organization = Organization .objects .get (slug = org )
210+ org_uuid = organization .id
211+ except Organization .DoesNotExist :
212+ self .logger .warning (f"Organization '{ org } ' not found" )
213+ return RadiusAccounting .objects .none ()
214+ else :
215+ # org is already an Organization object
216+ org_uuid = org .id
217+
218+ return RadiusAccounting .objects .filter (
219+ organization_id = org_uuid , called_station_id__in = unconverted_ids
220+ )
166221
167222
168223# monkey patching for openvpn_status begins
0 commit comments