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