blob: 2a7367f21f02eda3d1dee7b74248b90e9ef82701 [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
14import traceback
15from abc import abstractmethod
16from typing import Any, Dict
17
18from common.service import MagmaService
19import metrics
20from data_models.data_model_parameters import ParameterName
21from device_config.enodeb_configuration import EnodebConfiguration
22from exceptions import ConfigurationError
23from logger import EnodebdLogger as logger
24from state_machines.enb_acs import EnodebAcsStateMachine
25from state_machines.enb_acs_states import EnodebAcsState
26from state_machines.timer import StateMachineTimer
27from tr069 import models
28from tr069.models import Tr069ComplexModel
29
30
31class BasicEnodebAcsStateMachine(EnodebAcsStateMachine):
32 """
33 Most of the EnodebAcsStateMachine classes for each device work about the
34 same way. Differences lie mainly in the data model, desired configuration,
35 and the state transition map.
36
37 This class specifies the shared implementation between them.
38 """
39
40 # eNodeB connection timeout is used to determine whether or not eNodeB is
41 # connected to enodebd based on time of last Inform message. By default,
42 # periodic inform interval is 30secs, so timeout should be larger than
43 # this.
44 # Also set timer longer than reboot time, so that an eNodeB reboot does not
45 # trigger a connection-timeout alarm.
46 ENB_CONNECTION_TIMEOUT = 600 # In seconds
47
48 # If eNodeB is disconnected from MME for an unknown reason for this time,
49 # then reboot it. Set to a long time to ensure this doesn't interfere with
50 # other enodebd configuration processes - it is just a measure of last
51 # resort for an unlikely error case
52 MME_DISCONNECT_ENODEB_REBOOT_TIMER = 15 * 60
53
54 # Check the MME connection status every 15 seconds
55 MME_CHECK_TIMER = 15
56
57 def __init__(
58 self,
59 service: MagmaService,
60 use_param_key: bool,
61 ) -> None:
62 super().__init__(use_param_key=use_param_key)
63 self.state = None
64 self.timeout_handler = None
65 self.mme_timeout_handler = None
66 self.mme_timer = None
67 self._start_state_machine(service)
Wei-Yu Chen5cbdfbb2021-12-02 01:10:21 +080068
Wei-Yu Chen49950b92021-11-08 19:19:18 +080069
70 def get_state(self) -> str:
71 if self.state is None:
72 logger.warning('ACS State machine is not in any state.')
73 return 'N/A'
74 return self.state.state_description()
75
76 def handle_tr069_message(
77 self,
78 message: Tr069ComplexModel,
79 ) -> Tr069ComplexModel:
80 """
81 Accept the tr069 message from the eNB and produce a reply.
82
83 States may transition after reading a message but BEFORE producing
84 a reply. Most steps in the provisioning process are represented as
85 beginning with enodebd sending a request to the eNB, and waiting for
86 the reply from the eNB.
87 """
88 # TransferComplete messages come at random times, and we ignore them
89 if isinstance(message, models.TransferComplete):
90 return models.TransferCompleteResponse()
91 try:
92 self._read_tr069_msg(message)
93 return self._get_tr069_msg(message)
94 except Exception: # pylint: disable=broad-except
95 logger.error('Failed to handle tr069 message')
96 logger.error(traceback.format_exc())
97 self._dump_debug_info()
98 self.transition(self.unexpected_fault_state_name)
99 return self._get_tr069_msg(message)
100
101 def transition(self, next_state: str) -> Any:
102 logger.debug('State transition to <%s>', next_state)
103 self.state.exit()
104 self.state = self.state_map[next_state]
105 self.state.enter()
106
107 def stop_state_machine(self) -> None:
108 """ Clean up anything the state machine is tracking or doing """
109 self.state.exit()
110 if self.timeout_handler is not None:
111 self.timeout_handler.cancel()
112 self.timeout_handler = None
113 if self.mme_timeout_handler is not None:
114 self.mme_timeout_handler.cancel()
115 self.mme_timeout_handler = None
116 self._service = None
117 self._desired_cfg = None
118 self._device_cfg = None
119 self._data_model = None
120
121 self.mme_timer = None
122
123 def _start_state_machine(
124 self,
125 service: MagmaService,
126 ):
127 self.service = service
128 self.data_model = self.data_model_class()
129 # The current known device config has few known parameters
130 # The desired configuration depends on what the current configuration
131 # is. This we don't know fully, yet.
132 self.device_cfg = EnodebConfiguration(self.data_model)
133
134 self._init_state_map()
135 self.state = self.state_map[self.disconnected_state_name]
136 self.state.enter()
137 self._reset_timeout()
138 self._periodic_check_mme_connection()
139
140 def _reset_state_machine(
141 self,
142 service: MagmaService,
143 ):
144 self.stop_state_machine()
145 self._start_state_machine(service)
146
147 def _read_tr069_msg(self, message: Any) -> None:
148 """ Process incoming message and maybe transition state """
149 self._reset_timeout()
150 msg_handled, next_state = self.state.read_msg(message)
151 if not msg_handled:
152 self._transition_for_unexpected_msg(message)
153 _msg_handled, next_state = self.state.read_msg(message)
154 if next_state is not None:
155 self.transition(next_state)
156
157 def _get_tr069_msg(self, message: Any) -> Any:
158 """ Get a new message to send, and maybe transition state """
159 msg_and_transition = self.state.get_msg(message)
160 if msg_and_transition.next_state:
161 self.transition(msg_and_transition.next_state)
162 msg = msg_and_transition.msg
163 return msg
164
165 def _transition_for_unexpected_msg(self, message: Any) -> None:
166 """
167 eNB devices may send an Inform message in the middle of a provisioning
168 session. To deal with this, transition to a state that expects an
169 Inform message, but also track the status of the eNB as not having
170 been disconnected.
171 """
172 if isinstance(message, models.Inform):
173 logger.debug(
174 'ACS in (%s) state. Received an Inform message',
175 self.state.state_description(),
176 )
177 self._reset_state_machine(self.service)
178 elif isinstance(message, models.Fault):
179 logger.debug(
180 'ACS in (%s) state. Received a Fault <%s>',
181 self.state.state_description(), message.FaultString,
182 )
183 self.transition(self.unexpected_fault_state_name)
184 else:
185 raise ConfigurationError('Cannot handle unexpected TR069 msg')
186
187 def _reset_timeout(self) -> None:
188 if self.timeout_handler is not None:
189 self.timeout_handler.cancel()
190
191 def timed_out():
192 self.transition(self.disconnected_state_name)
193
194 self.timeout_handler = self.event_loop.call_later(
195 self.ENB_CONNECTION_TIMEOUT,
196 timed_out,
197 )
198
199 def _periodic_check_mme_connection(self) -> None:
200 self._check_mme_connection()
201 self.mme_timeout_handler = self.event_loop.call_later(
202 self.MME_CHECK_TIMER,
203 self._periodic_check_mme_connection,
204 )
205
206 def _check_mme_connection(self) -> None:
207 """
208 Check if eNodeB should be connected to MME but isn't, and maybe reboot.
209
210 If the eNB doesn't report connection to MME within a timeout period,
211 get it to reboot in the hope that it will fix things.
212
213 Usually, enodebd polls the eNodeB for whether it is connected to MME.
214 This method checks the last polled MME connection status, and if
215 eNodeB should be connected to MME but it isn't.
216 """
217 if self.device_cfg.has_parameter(ParameterName.MME_STATUS) and \
218 self.device_cfg.get_parameter(ParameterName.MME_STATUS):
219 is_mme_connected = 1
220 else:
221 is_mme_connected = 0
222
223 # True if we would expect MME to be connected, but it isn't
224 is_mme_unexpectedly_dc = \
225 self.is_enodeb_connected() \
226 and self.is_enodeb_configured() \
227 and self.mconfig.allow_enodeb_transmit \
228 and not is_mme_connected
229
230 if is_mme_unexpectedly_dc:
231 logger.warning(
232 'eNodeB is connected to AGw, is configured, '
233 'and has AdminState enabled for transmit. '
234 'MME connection to eNB is missing.',
235 )
236 if self.mme_timer is None:
237 logger.warning(
238 'eNodeB will be rebooted if MME connection '
239 'is not established in: %s seconds.',
240 self.MME_DISCONNECT_ENODEB_REBOOT_TIMER,
241 )
242 metrics.STAT_ENODEB_REBOOT_TIMER_ACTIVE.set(1)
243 self.mme_timer = \
244 StateMachineTimer(self.MME_DISCONNECT_ENODEB_REBOOT_TIMER)
245 elif self.mme_timer.is_done():
246 logger.warning(
247 'eNodeB has not established MME connection '
248 'within %s seconds - rebooting!',
249 self.MME_DISCONNECT_ENODEB_REBOOT_TIMER,
250 )
251 metrics.STAT_ENODEB_REBOOTS.labels(cause='MME disconnect').inc()
252 metrics.STAT_ENODEB_REBOOT_TIMER_ACTIVE.set(0)
253 self.mme_timer = None
254 self.reboot_asap()
255 else:
256 # eNB is not connected to MME, but we're still waiting to see
257 # if it will connect within the timeout period.
258 # Take no action for now.
259 pass
260 else:
261 if self.mme_timer is not None:
262 logger.info('eNodeB has established MME connection.')
263 self.mme_timer = None
264 metrics.STAT_ENODEB_REBOOT_TIMER_ACTIVE.set(0)
265
266 def _dump_debug_info(self) -> None:
267 if self.device_cfg is not None:
268 logger.error(
269 'Device configuration: %s',
270 self.device_cfg.get_debug_info(),
271 )
272 else:
273 logger.error('Device configuration: None')
274 if self.desired_cfg is not None:
275 logger.error(
276 'Desired configuration: %s',
277 self.desired_cfg.get_debug_info(),
278 )
279 else:
280 logger.error('Desired configuration: None')
281
282 @abstractmethod
283 def _init_state_map(self) -> None:
284 pass
285
286 @property
287 @abstractmethod
288 def state_map(self) -> Dict[str, EnodebAcsState]:
289 pass
290
291 @property
292 @abstractmethod
293 def disconnected_state_name(self) -> str:
294 pass
295
296 @property
297 @abstractmethod
298 def unexpected_fault_state_name(self) -> str:
299 """ State to handle unexpected Fault messages """
300 pass