1
1
import logging
2
2
import re
3
- from uuid import UUID
4
3
5
4
import openvpn_status
5
+ import swapper
6
6
from django .core .management import BaseCommand
7
7
from Exscript .protocols import telnetlib
8
8
from netaddr import EUI , mac_unix
9
9
10
10
from .... import settings as app_settings
11
11
from ....utils import load_model
12
12
13
- logger = logging .getLogger (__name__ )
14
-
15
13
RE_VIRTUAL_ADDR_MAC = re .compile ("^{0}:{0}:{0}:{0}:{0}:{0}" .format ("[a-f0-9]{2}" ), re .I )
16
14
TELNET_CONNECTION_TIMEOUT = 30 # In seconds
17
15
18
-
19
16
RadiusAccounting = load_model ("RadiusAccounting" )
20
17
21
18
19
+ logger = logging .getLogger (__name__ )
20
+
21
+
22
22
class 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
+
23
31
help = "Correct Called Station IDs of Radius Sessions"
24
32
25
33
def _get_raw_management_info (self , host , port , password ):
@@ -42,29 +50,29 @@ def _get_openvpn_routing_info(self, host, port=7505, password=None):
42
50
try :
43
51
raw_info = self ._get_raw_management_info (host , port , password )
44
52
except ConnectionRefusedError :
45
- logger .warning (
53
+ BaseConvertCalledStationIdCommand . logger .warning (
46
54
"Unable to establish telnet connection to "
47
55
f"{ host } on { port } . Skipping!"
48
56
)
49
57
return {}
50
58
except (OSError , TimeoutError , EOFError ) as error :
51
- logger .warning (
59
+ BaseConvertCalledStationIdCommand . logger .warning (
52
60
f"Error encountered while connecting to { host } :{ port } : { error } . "
53
61
"Skipping!"
54
62
)
55
63
return {}
56
64
except Exception :
57
- logger .warning (
65
+ BaseConvertCalledStationIdCommand . logger .warning (
58
66
f"Error encountered while connecting to { host } :{ port } . Skipping!"
59
67
)
60
68
return {}
61
69
try :
62
70
parsed_info = openvpn_status .parse_status (raw_info )
63
71
return parsed_info .routing_table
64
72
except openvpn_status .ParsingError as error :
65
- logger .warning (
73
+ BaseConvertCalledStationIdCommand . logger .warning (
66
74
"Unable to parse information received from "
67
- f"{ host } :{ port } . ParsingError: { error } . Skipping!" ,
75
+ f"{ host } :{ port } . ParsingError: { error } . Skipping!"
68
76
)
69
77
return {}
70
78
@@ -74,7 +82,7 @@ def _get_radius_session(self, unique_id):
74
82
unique_id = unique_id
75
83
)
76
84
except RadiusAccounting .DoesNotExist :
77
- logger .warning (
85
+ BaseConvertCalledStationIdCommand . logger .warning (
78
86
f'RadiusAccount object with unique_id "{ unique_id } " does not exist.'
79
87
)
80
88
@@ -88,24 +96,11 @@ def _get_called_station_setting(self, radius_session):
88
96
# but will removed in future versions
89
97
return {org_id : app_settings .CALLED_STATION_IDS [organization .slug ]}
90
98
except KeyError :
91
- logger .error (
99
+ BaseConvertCalledStationIdCommand . logger .error (
92
100
"OPENWISP_RADIUS_CALLED_STATION_IDS does not contain setting "
93
101
f'for "{ radius_session .organization .name } " organization'
94
102
)
95
103
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
-
109
104
def add_arguments (self , parser ):
110
105
parser .add_argument ("--unique_id" , action = "store" , type = str , default = "" )
111
106
@@ -128,41 +123,101 @@ def handle(self, *args, **options):
128
123
for org , config in called_station_id_setting .items ():
129
124
routing_dict = {}
130
125
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 ),
137
131
)
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 )
138
140
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
+ )
140
144
continue
141
145
142
146
if unique_id :
143
147
qs = [input_radius_session ]
144
148
else :
145
149
qs = self ._get_unconverted_sessions (org , config ["unconverted_ids" ])
146
150
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 (
156
173
"Failed to find routing information for "
157
174
f"{ radius_session .session_id } . Skipping!"
158
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 )
159
182
except (TypeError , IndexError ):
160
- logger .warning (
183
+ BaseConvertCalledStationIdCommand . logger .warning (
161
184
f'Failed to find a MAC address in "{ common_name } ". '
162
185
f"Skipping { radius_session .session_id } !"
163
186
)
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
+ )
166
221
167
222
168
223
# monkey patching for openvpn_status begins
0 commit comments