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