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-
1515RE_VIRTUAL_ADDR_MAC = re .compile ("^{0}:{0}:{0}:{0}:{0}:{0}" .format ("[a-f0-9]{2}" ), re .I )
1616TELNET_CONNECTION_TIMEOUT = 30 # In seconds
1717
18-
1918RadiusAccounting = load_model ("RadiusAccounting" )
2019
20+ logger = logging .getLogger (__name__ )
21+
2122
2223class BaseConvertCalledStationIdCommand (BaseCommand ):
24+ logger = logger
25+
26+ def _search_mac_address (self , common_name ):
27+ match = RE_VIRTUAL_ADDR_MAC .search (common_name )
28+ if not match :
29+ raise IndexError (f"No MAC address found in '{ common_name } '" )
30+ return match [0 ]
31+
2332 help = "Correct Called Station IDs of Radius Sessions"
2433
2534 def _get_raw_management_info (self , host , port , password ):
@@ -42,29 +51,29 @@ def _get_openvpn_routing_info(self, host, port=7505, password=None):
4251 try :
4352 raw_info = self ._get_raw_management_info (host , port , password )
4453 except ConnectionRefusedError :
45- logger .warning (
54+ BaseConvertCalledStationIdCommand . logger .warning (
4655 "Unable to establish telnet connection to "
4756 f"{ host } on { port } . Skipping!"
4857 )
4958 return {}
5059 except (OSError , TimeoutError , EOFError ) as error :
51- logger .warning (
60+ BaseConvertCalledStationIdCommand . logger .warning (
5261 f"Error encountered while connecting to { host } :{ port } : { error } . "
5362 "Skipping!"
5463 )
5564 return {}
5665 except Exception :
57- logger .warning (
66+ BaseConvertCalledStationIdCommand . logger .warning (
5867 f"Error encountered while connecting to { host } :{ port } . Skipping!"
5968 )
6069 return {}
6170 try :
6271 parsed_info = openvpn_status .parse_status (raw_info )
6372 return parsed_info .routing_table
6473 except openvpn_status .ParsingError as error :
65- logger .warning (
74+ BaseConvertCalledStationIdCommand . logger .warning (
6675 "Unable to parse information received from "
67- f"{ host } :{ port } . ParsingError: { error } . Skipping!" ,
76+ f"{ host } :{ port } . ParsingError: { error } . Skipping!"
6877 )
6978 return {}
7079
@@ -74,7 +83,7 @@ def _get_radius_session(self, unique_id):
7483 unique_id = unique_id
7584 )
7685 except RadiusAccounting .DoesNotExist :
77- logger .warning (
86+ BaseConvertCalledStationIdCommand . logger .warning (
7887 f'RadiusAccount object with unique_id "{ unique_id } " does not exist.'
7988 )
8089
@@ -88,24 +97,11 @@ def _get_called_station_setting(self, radius_session):
8897 # but will removed in future versions
8998 return {org_id : app_settings .CALLED_STATION_IDS [organization .slug ]}
9099 except KeyError :
91- logger .error (
100+ BaseConvertCalledStationIdCommand . logger .error (
92101 "OPENWISP_RADIUS_CALLED_STATION_IDS does not contain setting "
93102 f'for "{ radius_session .organization .name } " organization'
94103 )
95104
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-
109105 def add_arguments (self , parser ):
110106 parser .add_argument ("--unique_id" , action = "store" , type = str , default = "" )
111107
@@ -128,15 +124,24 @@ def handle(self, *args, **options):
128124 for org , config in called_station_id_setting .items ():
129125 routing_dict = {}
130126 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- )
127+ raw_routing = self .__class__ ._get_openvpn_routing_info (
128+ self ,
129+ openvpn_config ["host" ],
130+ openvpn_config .get ("port" , 7505 ),
131+ openvpn_config .get ("password" , None ),
137132 )
133+ normalized_routing = {}
134+ for k , v in raw_routing .items ():
135+ try :
136+ norm_key = str (EUI (k , dialect = mac_unix )).lower ()
137+ except Exception :
138+ norm_key = k .lower ()
139+ normalized_routing [norm_key ] = v
140+ routing_dict .update (normalized_routing )
138141 if not routing_dict :
139- logger .info (f'No routing information found for "{ org } " organization' )
142+ BaseConvertCalledStationIdCommand .logger .info (
143+ f'No routing information found for "{ org } " organization'
144+ )
140145 continue
141146
142147 if unique_id :
@@ -145,24 +150,68 @@ def handle(self, *args, **options):
145150 qs = self ._get_unconverted_sessions (org , config ["unconverted_ids" ])
146151 for radius_session in qs :
147152 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 "
153+ lookup_key = str (
154+ EUI (radius_session .calling_station_id , dialect = mac_unix )
155+ ).lower ()
156+ except (AddrFormatError , ValueError , TypeError ):
157+ BaseConvertCalledStationIdCommand .logger .warning (
158+ f"Invalid calling_station_id for session "
157159 f"{ radius_session .session_id } . Skipping!"
158160 )
161+ continue
162+ if lookup_key not in routing_dict :
163+
164+ def _strip_leading_zeros (k ):
165+ parts = k .split (":" )
166+ return ":" .join ([p .lstrip ("0" ) or "0" for p in parts ])
167+
168+ alt_key = _strip_leading_zeros (lookup_key )
169+ if alt_key in routing_dict :
170+ routing_dict [lookup_key ] = routing_dict [alt_key ]
171+ else :
172+ BaseConvertCalledStationIdCommand .logger .warning (
173+ "Failed to find routing information for "
174+ f"{ radius_session .session_id } . Skipping!"
175+ )
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+ radius_session .called_station_id = sanitize_mac_address (mac_address )
189+ radius_session .save ()
190+
191+ def _get_unconverted_sessions (self , org , unconverted_ids ):
192+ """
193+ Get unconverted sessions for the given organization and unconverted IDs.
194+ """
195+
196+ if isinstance (org , str ):
197+ try :
198+ org_uuid = UUID (org )
199+ except ValueError :
200+ Organization = swapper .load_model ("openwisp_users" , "Organization" )
201+ try :
202+ organization = Organization .objects .get (slug = org )
203+ org_uuid = organization .id
204+ except Organization .DoesNotExist :
205+ self .logger .warning (f"Organization '{ org } ' not found" )
206+ return RadiusAccounting .objects .none ()
207+ else :
208+ org_uuid = org .id
209+
210+ return RadiusAccounting .objects .filter (
211+ organization_id = org_uuid ,
212+ called_station_id__in = unconverted_ids ,
213+ stop_time__isnull = True ,
214+ )
166215
167216
168217# monkey patching for openvpn_status begins
0 commit comments