blob: ba6ae278a1ac641998f052ddadffe316cb675717 [file] [log] [blame]
Wei-Yu Chenad55cb82022-02-15 20:07:01 +08001# SPDX-FileCopyrightText: 2020 The Magma Authors.
2# SPDX-FileCopyrightText: 2022 Open Networking Foundation <support@opennetworking.org>
3#
4# SPDX-License-Identifier: BSD-3-Clause
Wei-Yu Chen49950b92021-11-08 19:19:18 +08005
Wei-Yu Chenb91af852022-03-15 22:24:49 +08006import time
Wei-Yu Chen49950b92021-11-08 19:19:18 +08007from typing import Any, List, Optional
8
9from common.service import MagmaService
10from device_config.configuration_util import is_enb_registered
11from devices.device_map import get_device_handler_from_name
12from devices.device_utils import EnodebDeviceName
13from exceptions import UnrecognizedEnodebError
14from logger import EnodebdLogger as logger
15from state_machines.acs_state_utils import (
16 get_device_name_from_inform,
17)
18from state_machines.enb_acs import EnodebAcsStateMachine
19from tr069 import models
20from spyne import ComplexModelBase
21from spyne.server.wsgi import WsgiMethodContext
22
23
24class StateMachineManager:
25 """
26 Delegates tr069 message handling to a dedicated state machine for the
27 device.
28 """
29
30 def __init__(
31 self,
32 service: MagmaService,
33 ):
34 self._ip_serial_mapping = IpToSerialMapping()
35 self._service = service
36 self._state_machine_by_ip = {}
37
38 def handle_tr069_message(
39 self,
40 ctx: WsgiMethodContext,
41 tr069_message: ComplexModelBase,
42 ) -> Any:
43 """ Delegate message handling to the appropriate eNB state machine """
44 client_ip = self._get_client_ip(ctx)
45 if isinstance(tr069_message, models.Inform):
46 try:
47 self._update_device_mapping(client_ip, tr069_message)
48 except UnrecognizedEnodebError as err:
49 logger.warning(
50 'Received TR-069 Inform message from an '
51 'unrecognized device. '
52 'Ending TR-069 session with empty HTTP '
53 'response. Error: (%s)', err,
54 )
55 return models.DummyInput()
56
57 handler = self._get_handler(client_ip)
Wei-Yu Chenad55cb82022-02-15 20:07:01 +080058
Wei-Yu Chen49950b92021-11-08 19:19:18 +080059 if handler is None:
60 logger.warning(
61 'Received non-Inform TR-069 message from unknown '
62 'eNB. Ending session with empty HTTP response.',
63 )
64 return models.DummyInput()
65
66 return handler.handle_tr069_message(tr069_message)
67
68 def get_handler_by_ip(self, client_ip: str) -> EnodebAcsStateMachine:
69 return self._state_machine_by_ip[client_ip]
70
71 def get_handler_by_serial(self, enb_serial: str) -> EnodebAcsStateMachine:
72 client_ip = self._ip_serial_mapping.get_ip(enb_serial)
73 return self._state_machine_by_ip[client_ip]
74
75 def get_connected_serial_id_list(self) -> List[str]:
76 return self._ip_serial_mapping.get_serial_list()
77
78 def get_ip_of_serial(self, enb_serial: str) -> str:
79 return self._ip_serial_mapping.get_ip(enb_serial)
80
81 def get_serial_of_ip(self, client_ip: str) -> str:
82 serial = self._ip_serial_mapping.get_serial(client_ip)
83 return serial or 'default'
84
85 def _get_handler(
86 self,
87 client_ip: str,
88 ) -> EnodebAcsStateMachine:
89 return self._state_machine_by_ip[client_ip]
90
91 def _update_device_mapping(
92 self,
93 client_ip: str,
94 inform: models.Inform,
95 ) -> None:
96 """
97 When receiving an Inform message, we can figure out what device we
98 are talking to. We can also see if the IP has changed, and the
99 StateMachineManager must track this so that subsequent tr069
100 messages can be handled correctly.
101 """
102 enb_serial = self._parse_msg_for_serial(inform)
103 if enb_serial is None:
104 raise UnrecognizedEnodebError(
105 'eNB does not have serial number '
106 'under expected param path',
107 )
108 if not is_enb_registered(self._service.mconfig, enb_serial):
109 raise UnrecognizedEnodebError(
110 'eNB not registered to this Access '
111 'Gateway (serial #%s)' % enb_serial,
112 )
113 self._associate_serial_to_ip(client_ip, enb_serial)
114 handler = self._get_handler(client_ip)
115 if handler is None:
116 device_name = get_device_name_from_inform(inform)
117 handler = self._build_handler(device_name)
118 self._state_machine_by_ip[client_ip] = handler
119
120 def _associate_serial_to_ip(
121 self,
122 client_ip: str,
123 enb_serial: str,
124 ) -> None:
125 """
126 If a device/IP combination changes, then the StateMachineManager
127 must detect this, and update its mapping of what serial/IP corresponds
128 to which handler.
129 """
130 if self._ip_serial_mapping.has_ip(client_ip):
131 # Same IP, different eNB connected
132 prev_serial = self._ip_serial_mapping.get_serial(client_ip)
133 if enb_serial != prev_serial:
134 logger.info(
135 'eNodeB change on IP <%s>, from %s to %s',
136 client_ip, prev_serial, enb_serial,
137 )
138 self._ip_serial_mapping.set_ip_and_serial(client_ip, enb_serial)
139 self._state_machine_by_ip[client_ip] = None
140 elif self._ip_serial_mapping.has_serial(enb_serial):
141 # Same eNB, different IP
142 prev_ip = self._ip_serial_mapping.get_ip(enb_serial)
143 if client_ip != prev_ip:
144 logger.info(
145 'eNodeB <%s> changed IP from %s to %s',
146 enb_serial, prev_ip, client_ip,
147 )
148 self._ip_serial_mapping.set_ip_and_serial(client_ip, enb_serial)
149 handler = self._state_machine_by_ip[prev_ip]
150 self._state_machine_by_ip[client_ip] = handler
151 del self._state_machine_by_ip[prev_ip]
152 else:
153 # TR069 message is coming from a different IP, and a different
154 # serial ID. No need to change mapping
155 handler = None
156 self._ip_serial_mapping.set_ip_and_serial(client_ip, enb_serial)
157 self._state_machine_by_ip[client_ip] = handler
158
159 @staticmethod
160 def _parse_msg_for_serial(tr069_message: models.Inform) -> Optional[str]:
161 """ Return the eNodeB serial ID if it's found in the message """
162 if not isinstance(tr069_message, models.Inform):
163 return
164
165 # Mikrotik Intercell does not return serial in ParameterList
166 if hasattr(tr069_message, 'DeviceId') and \
167 hasattr(tr069_message.DeviceId, 'SerialNumber'):
168 return tr069_message.DeviceId.SerialNumber
169
170 if not hasattr(tr069_message, 'ParameterList') or \
171 not hasattr(tr069_message.ParameterList, 'ParameterValueStruct'):
172 return None
173
174 # Parse the parameters
175 param_values_by_path = {}
176 for param_value in tr069_message.ParameterList.ParameterValueStruct:
177 path = param_value.Name
178 value = param_value.Value.Data
179 param_values_by_path[path] = value
180
181 possible_sn_paths = [
182 'Device.DeviceInfo.SerialNumber',
183 'InternetGatewayDevice.DeviceInfo.SerialNumber',
184 ]
185 for path in possible_sn_paths:
186 if path in param_values_by_path:
187 return param_values_by_path[path]
188 return None
189
190 @staticmethod
191 def _get_client_ip(ctx: WsgiMethodContext) -> str:
Wei-Yu Chenad55cb82022-02-15 20:07:01 +0800192
193 client_ip = ctx.transport.req_env.get("HTTP_X_REAL_IP",
194 ctx.transport.req_env.get("REMOTE_ADDR", "unknown")
195 )
196
197 return client_ip
Wei-Yu Chen49950b92021-11-08 19:19:18 +0800198
199 def _build_handler(
200 self,
201 device_name: EnodebDeviceName,
202 ) -> EnodebAcsStateMachine:
203 """
204 Create a new state machine based on the device type
205 """
206 device_handler_class = get_device_handler_from_name(device_name)
207 acs_state_machine = device_handler_class(self._service)
208 return acs_state_machine
209
210
211class IpToSerialMapping:
212 """ Bidirectional map between <eNodeB IP> and <eNodeB serial ID> """
213
214 def __init__(self) -> None:
215 self.ip_by_enb_serial = {}
216 self.enb_serial_by_ip = {}
217
218 def del_ip(self, ip: str) -> None:
219 if ip not in self.enb_serial_by_ip:
220 raise KeyError('Cannot delete missing IP')
221 serial = self.enb_serial_by_ip[ip]
222 del self.enb_serial_by_ip[ip]
223 del self.ip_by_enb_serial[serial]
224
225 def del_serial(self, serial: str) -> None:
226 if serial not in self.ip_by_enb_serial:
227 raise KeyError('Cannot delete missing eNodeB serial ID')
228 ip = self.ip_by_enb_serial[serial]
229 del self.ip_by_enb_serial[serial]
230 del self.enb_serial_by_ip[ip]
231
232 def set_ip_and_serial(self, ip: str, serial: str) -> None:
233 self.ip_by_enb_serial[serial] = ip
234 self.enb_serial_by_ip[ip] = serial
235
236 def get_ip(self, serial: str) -> str:
237 return self.ip_by_enb_serial[serial]
238
239 def get_serial(self, ip: str) -> Optional[str]:
240 return self.enb_serial_by_ip.get(ip, None)
241
242 def has_ip(self, ip: str) -> bool:
243 return ip in self.enb_serial_by_ip
244
245 def has_serial(self, serial: str) -> bool:
246 return serial in self.ip_by_enb_serial
247
248 def get_serial_list(self) -> List[str]:
249 return list(self.ip_by_enb_serial.keys())
250
251 def get_ip_list(self) -> List[str]:
252 return list(self.enb_serial_by_ip.keys())