blob: bfd082410b82b31d50f05d1bfde935fc92ea620d [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 Chen678f0a52021-12-21 13:50:52 +08005
6import time
Wei-Yu Chen49950b92021-11-08 19:19:18 +08007from abc import ABC, abstractmethod
8from collections import namedtuple
9from typing import Any, Optional
10
Wei-Yu Chen678f0a52021-12-21 13:50:52 +080011import metrics
12
13from configuration.service_configs import load_service_config
Wei-Yu Chen49950b92021-11-08 19:19:18 +080014from data_models.data_model import InvalidTrParamPath
15from data_models.data_model_parameters import ParameterName
16from device_config.configuration_init import build_desired_config
17from exceptions import ConfigurationError, Tr069Error
18from logger import EnodebdLogger as logger
19from state_machines.acs_state_utils import (
20 does_inform_have_event,
21 get_all_objects_to_add,
22 get_all_objects_to_delete,
23 get_all_param_values_to_set,
24 get_obj_param_values_to_set,
25 get_object_params_to_get,
26 get_optional_param_to_check,
27 get_param_values_to_set,
28 get_params_to_get,
29 parse_get_parameter_values_response,
30 process_inform_message,
31)
32from state_machines.enb_acs import EnodebAcsStateMachine
33from state_machines.timer import StateMachineTimer
34from tr069 import models
35
36AcsMsgAndTransition = namedtuple(
37 'AcsMsgAndTransition', ['msg', 'next_state'],
38)
39
40AcsReadMsgResult = namedtuple(
41 'AcsReadMsgResult', ['msg_handled', 'next_state'],
42)
43
44
45class EnodebAcsState(ABC):
46 """
47 State class for the Enodeb state machine
48
49 States can transition after reading a message from the eNB, sending a
50 message out to the eNB, or when a timer completes. As such, some states
51 are only responsible for message sending, and others are only responsible
52 for reading incoming messages.
53
54 In the constructor, set up state transitions.
55 """
56
57 def __init__(self):
58 self._acs = None
59
60 def enter(self) -> None:
61 """
62 Set up your timers here. Call transition(..) on the ACS when the timer
63 completes or throw an error
64 """
65 pass
66
67 def exit(self) -> None:
68 """Destroy timers here"""
69 pass
70
71 def read_msg(self, message: Any) -> AcsReadMsgResult:
72 """
73 Args: message: tr069 message
74 Returns: name of the next state, if transition required
75 """
76 raise ConfigurationError(
77 '%s should implement read_msg() if it '
78 'needs to handle message reading' % self.__class__.__name__,
79 )
80
81 def get_msg(self, message: Any) -> AcsMsgAndTransition:
82 """
83 Produce a message to send back to the eNB.
84
85 Args:
86 message: TR-069 message which was already processed by read_msg
87
88 Returns: Message and possible transition
89 """
90 raise ConfigurationError(
91 '%s should implement get_msg() if it '
92 'needs to produce messages' % self.__class__.__name__,
93 )
94
95 @property
96 def acs(self) -> EnodebAcsStateMachine:
97 return self._acs
98
99 @acs.setter
100 def acs(self, val: EnodebAcsStateMachine) -> None:
101 self._acs = val
102
103 @abstractmethod
104 def state_description(self) -> str:
105 """ Provide a few words about what the state represents """
106 pass
107
108
109class WaitInformState(EnodebAcsState):
110 """
111 This state indicates that no Inform message has been received yet, or
112 that no Inform message has been received for a long time.
113
114 This state is used to handle an Inform message that arrived when enodebd
115 already believes that the eNB is connected. As such, it is unclear to
116 enodebd whether the eNB is just sending another Inform, or if a different
117 eNB was plugged into the same interface.
118 """
119
120 def __init__(
121 self,
122 acs: EnodebAcsStateMachine,
123 when_done: str,
124 when_boot: Optional[str] = None,
125 ):
126 super().__init__()
127 self.acs = acs
128 self.done_transition = when_done
129 self.boot_transition = when_boot
130 self.has_enb_just_booted = False
131
132 def read_msg(self, message: Any) -> AcsReadMsgResult:
133 """
134 Args:
135 message: models.Inform Tr069 Inform message
136 """
137 if not isinstance(message, models.Inform):
138 return AcsReadMsgResult(False, None)
139 process_inform_message(
140 message, self.acs.data_model,
141 self.acs.device_cfg,
142 )
143 if does_inform_have_event(message, '1 BOOT'):
144 return AcsReadMsgResult(True, self.boot_transition)
145 return AcsReadMsgResult(True, None)
146
147 def get_msg(self, message: Any) -> AcsMsgAndTransition:
148 """ Reply with InformResponse """
149 response = models.InformResponse()
150 # Set maxEnvelopes to 1, as per TR-069 spec
151 response.MaxEnvelopes = 1
152 return AcsMsgAndTransition(response, self.done_transition)
153
154 def state_description(self) -> str:
155 return 'Waiting for an Inform'
156
157
158class GetRPCMethodsState(EnodebAcsState):
159 """
160 After the first Inform message from boot, it is expected that the eNB
161 will try to learn the RPC methods of the ACS.
162 """
163
164 def __init__(self, acs: EnodebAcsStateMachine, when_done: str, when_skip: str):
165 super().__init__()
166 self.acs = acs
167 self.done_transition = when_done
168 self.skip_transition = when_skip
169
170 def read_msg(self, message: Any) -> AcsReadMsgResult:
171 # If this is a regular Inform, not after a reboot we'll get an empty
172 if isinstance(message, models.DummyInput):
173 return AcsReadMsgResult(True, self.skip_transition)
174 if not isinstance(message, models.GetRPCMethods):
175 return AcsReadMsgResult(False, self.done_transition)
176 return AcsReadMsgResult(True, None)
177
178 def get_msg(self, message: Any) -> AcsMsgAndTransition:
179 resp = models.GetRPCMethodsResponse()
180 resp.MethodList = models.MethodList()
181 RPC_METHODS = ['Inform', 'GetRPCMethods', 'TransferComplete']
182 resp.MethodList.arrayType = 'xsd:string[%d]' \
183 % len(RPC_METHODS)
184 resp.MethodList.string = RPC_METHODS
185 return AcsMsgAndTransition(resp, self.done_transition)
186
187 def state_description(self) -> str:
188 return 'Waiting for incoming GetRPC Methods after boot'
189
190
191class BaicellsRemWaitState(EnodebAcsState):
192 """
193 We've already received an Inform message. This state is to handle a
194 Baicells eNodeB issue.
195
196 After eNodeB is rebooted, hold off configuring it for some time to give
197 time for REM to run. This is a BaiCells eNodeB issue that doesn't support
198 enabling the eNodeB during initial REM.
199
200 In this state, just hang at responding to Inform, and then ending the
201 TR-069 session.
202 """
203
204 CONFIG_DELAY_AFTER_BOOT = 600
205
206 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
207 super().__init__()
208 self.acs = acs
209 self.done_transition = when_done
210 self.rem_timer = None
211
212 def enter(self):
213 self.rem_timer = StateMachineTimer(self.CONFIG_DELAY_AFTER_BOOT)
214 logger.info(
215 'Holding off of eNB configuration for %s seconds. '
216 'Will resume after eNB REM process has finished. ',
217 self.CONFIG_DELAY_AFTER_BOOT,
218 )
219
220 def exit(self):
221 self.rem_timer = None
222
223 def read_msg(self, message: Any) -> AcsReadMsgResult:
224 if not isinstance(message, models.Inform):
225 return AcsReadMsgResult(False, None)
226 process_inform_message(
227 message, self.acs.data_model,
228 self.acs.device_cfg,
229 )
230 return AcsReadMsgResult(True, None)
231
232 def get_msg(self, message: Any) -> AcsMsgAndTransition:
233 if self.rem_timer.is_done():
234 return AcsMsgAndTransition(
235 models.DummyInput(),
236 self.done_transition,
237 )
238 return AcsMsgAndTransition(models.DummyInput(), None)
239
240 def state_description(self) -> str:
241 remaining = self.rem_timer.seconds_remaining()
242 return 'Waiting for eNB REM to run for %d more seconds before ' \
243 'resuming with configuration.' % remaining
244
245
246class WaitEmptyMessageState(EnodebAcsState):
247 def __init__(
248 self,
249 acs: EnodebAcsStateMachine,
250 when_done: str,
251 when_missing: Optional[str] = None,
252 ):
253 super().__init__()
254 self.acs = acs
255 self.done_transition = when_done
256 self.unknown_param_transition = when_missing
257
258 def read_msg(self, message: Any) -> AcsReadMsgResult:
259 """
260 It's expected that we transition into this state right after receiving
261 an Inform message and replying with an InformResponse. At that point,
262 the eNB sends an empty HTTP request (aka DummyInput) to initiate the
263 rest of the provisioning process
264 """
265 if not isinstance(message, models.DummyInput):
266 logger.debug("Ignoring message %s", str(type(message)))
267 return AcsReadMsgResult(msg_handled=False, next_state=None)
268 if self.unknown_param_transition:
269 if get_optional_param_to_check(self.acs.data_model):
270 return AcsReadMsgResult(
271 msg_handled=True,
272 next_state=self.unknown_param_transition,
273 )
274 return AcsReadMsgResult(
275 msg_handled=True,
276 next_state=self.done_transition,
277 )
278
279 def get_msg(self, message: Any) -> AcsReadMsgResult:
280 """
281 Return a dummy message waiting for the empty message from CPE
282 """
283 request = models.DummyInput()
284 return AcsMsgAndTransition(msg=request, next_state=None)
285
286 def state_description(self) -> str:
287 return 'Waiting for empty message from eNodeB'
288
289
290class CheckOptionalParamsState(EnodebAcsState):
291 def __init__(
292 self,
293 acs: EnodebAcsStateMachine,
294 when_done: str,
295 ):
296 super().__init__()
297 self.acs = acs
298 self.done_transition = when_done
299 self.optional_param = None
300
301 def get_msg(self, message: Any) -> AcsMsgAndTransition:
302 self.optional_param = get_optional_param_to_check(self.acs.data_model)
303 if self.optional_param is None:
304 raise Tr069Error('Invalid State')
305 # Generate the request
306 request = models.GetParameterValues()
307 request.ParameterNames = models.ParameterNames()
308 request.ParameterNames.arrayType = 'xsd:string[1]'
309 request.ParameterNames.string = []
310 path = self.acs.data_model.get_parameter(self.optional_param).path
311 request.ParameterNames.string.append(path)
312 return AcsMsgAndTransition(request, None)
313
314 def read_msg(self, message: Any) -> AcsReadMsgResult:
315 """ Process either GetParameterValuesResponse or a Fault """
316 if type(message) == models.Fault:
317 self.acs.data_model.set_parameter_presence(
318 self.optional_param,
319 False,
320 )
321 elif type(message) == models.GetParameterValuesResponse:
322 name_to_val = parse_get_parameter_values_response(
323 self.acs.data_model,
324 message,
325 )
326 logger.debug(
327 'Received CPE parameter values: %s',
328 str(name_to_val),
329 )
330 for name, val in name_to_val.items():
331 self.acs.data_model.set_parameter_presence(
332 self.optional_param,
333 True,
334 )
335 magma_val = self.acs.data_model.transform_for_magma(name, val)
336 self.acs.device_cfg.set_parameter(name, magma_val)
337 else:
338 return AcsReadMsgResult(False, None)
339
340 if get_optional_param_to_check(self.acs.data_model) is not None:
341 return AcsReadMsgResult(True, None)
342 return AcsReadMsgResult(True, self.done_transition)
343
344 def state_description(self) -> str:
345 return 'Checking if some optional parameters exist in data model'
346
347
348class SendGetTransientParametersState(EnodebAcsState):
349 """
350 Periodically read eNodeB status. Note: keep frequency low to avoid
351 backing up large numbers of read operations if enodebd is busy.
352 Some eNB parameters are read only and updated by the eNB itself.
353 """
354 PARAMETERS = [
355 ParameterName.OP_STATE,
356 ParameterName.RF_TX_STATUS,
357 ParameterName.GPS_STATUS,
358 ParameterName.PTP_STATUS,
359 ParameterName.MME_STATUS,
360 ParameterName.GPS_LAT,
361 ParameterName.GPS_LONG,
362 ]
363
364 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
365 super().__init__()
366 self.acs = acs
367 self.done_transition = when_done
368
369 def read_msg(self, message: Any) -> AcsReadMsgResult:
370 if not isinstance(message, models.DummyInput):
371 return AcsReadMsgResult(False, None)
372 return AcsReadMsgResult(True, None)
373
374 def get_msg(self, message: Any) -> AcsMsgAndTransition:
375 request = models.GetParameterValues()
376 request.ParameterNames = models.ParameterNames()
377 request.ParameterNames.string = []
378 for name in self.PARAMETERS:
379 # Not all data models have these parameters
380 if self.acs.data_model.is_parameter_present(name):
381 path = self.acs.data_model.get_parameter(name).path
382 request.ParameterNames.string.append(path)
383 request.ParameterNames.arrayType = \
384 'xsd:string[%d]' % len(request.ParameterNames.string)
385
386 return AcsMsgAndTransition(request, self.done_transition)
387
388 def state_description(self) -> str:
389 return 'Getting transient read-only parameters'
390
391
392class WaitGetTransientParametersState(EnodebAcsState):
393 """
394 Periodically read eNodeB status. Note: keep frequency low to avoid
395 backing up large numbers of read operations if enodebd is busy
396 """
397
398 def __init__(
399 self,
400 acs: EnodebAcsStateMachine,
401 when_get: str,
402 when_get_obj_params: str,
403 when_delete: str,
404 when_add: str,
405 when_set: str,
406 when_skip: str,
407 ):
408 super().__init__()
409 self.acs = acs
410 self.done_transition = when_get
411 self.get_obj_params_transition = when_get_obj_params
412 self.rm_obj_transition = when_delete
413 self.add_obj_transition = when_add
414 self.set_transition = when_set
415 self.skip_transition = when_skip
416
417 def read_msg(self, message: Any) -> AcsReadMsgResult:
418 if not isinstance(message, models.GetParameterValuesResponse):
419 return AcsReadMsgResult(False, None)
420 # Current values of the fetched parameters
421 name_to_val = parse_get_parameter_values_response(
422 self.acs.data_model,
423 message,
424 )
425 logger.debug('Fetched Transient Params: %s', str(name_to_val))
426
427 # Update device configuration
428 for name in name_to_val:
429 magma_val = \
430 self.acs.data_model.transform_for_magma(
431 name,
432 name_to_val[name],
433 )
434 self.acs.device_cfg.set_parameter(name, magma_val)
435
436 return AcsReadMsgResult(True, self.get_next_state())
437
438 def get_next_state(self) -> str:
439 should_get_params = \
440 len(
441 get_params_to_get(
442 self.acs.device_cfg,
443 self.acs.data_model,
444 ),
445 ) > 0
446 if should_get_params:
447 return self.done_transition
448 should_get_obj_params = \
449 len(
450 get_object_params_to_get(
451 self.acs.desired_cfg,
452 self.acs.device_cfg,
453 self.acs.data_model,
454 ),
455 ) > 0
456 if should_get_obj_params:
457 return self.get_obj_params_transition
458 elif len(
459 get_all_objects_to_delete(
460 self.acs.desired_cfg,
461 self.acs.device_cfg,
462 ),
463 ) > 0:
464 return self.rm_obj_transition
465 elif len(
466 get_all_objects_to_add(
467 self.acs.desired_cfg,
468 self.acs.device_cfg,
469 ),
470 ) > 0:
471 return self.add_obj_transition
472 return self.skip_transition
473
474 def state_description(self) -> str:
475 return 'Getting transient read-only parameters'
476
477
478class GetParametersState(EnodebAcsState):
479 """
480 Get the value of most parameters of the eNB that are defined in the data
481 model. Object parameters are excluded.
482 """
483
484 def __init__(
485 self,
486 acs: EnodebAcsStateMachine,
487 when_done: str,
488 request_all_params: bool = False,
489 ):
490 super().__init__()
491 self.acs = acs
492 self.done_transition = when_done
493 # Set to True if we want to request values of all parameters, even if
494 # the ACS state machine already has recorded values of them.
495 self.request_all_params = request_all_params
496
497 def read_msg(self, message: Any) -> AcsReadMsgResult:
498 """
499 It's expected that we transition into this state right after receiving
500 an Inform message and replying with an InformResponse. At that point,
501 the eNB sends an empty HTTP request (aka DummyInput) to initiate the
502 rest of the provisioning process
503 """
504 if not isinstance(message, models.DummyInput):
505 return AcsReadMsgResult(False, None)
506 return AcsReadMsgResult(True, None)
507
508 def get_msg(self, message: Any) -> AcsMsgAndTransition:
509 """
510 Respond with GetParameterValuesRequest
511
512 Get the values of all parameters defined in the data model.
513 Also check which addable objects are present, and what the values of
514 parameters for those objects are.
515 """
516
517 # Get the names of regular parameters
518 names = get_params_to_get(
519 self.acs.device_cfg, self.acs.data_model,
520 self.request_all_params,
521 )
522
523 # Generate the request
524 request = models.GetParameterValues()
525 request.ParameterNames = models.ParameterNames()
526 request.ParameterNames.arrayType = 'xsd:string[%d]' \
527 % len(names)
528 request.ParameterNames.string = []
529 for name in names:
530 path = self.acs.data_model.get_parameter(name).path
531 if path is not InvalidTrParamPath:
532 # Only get data elements backed by tr69 path
533 request.ParameterNames.string.append(path)
534
535 return AcsMsgAndTransition(request, self.done_transition)
536
537 def state_description(self) -> str:
538 return 'Getting non-object parameters'
539
540
541class WaitGetParametersState(EnodebAcsState):
542 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
543 super().__init__()
544 self.acs = acs
545 self.done_transition = when_done
546
547 def read_msg(self, message: Any) -> AcsReadMsgResult:
548 """ Process GetParameterValuesResponse """
549 if not isinstance(message, models.GetParameterValuesResponse):
550 return AcsReadMsgResult(False, None)
551 name_to_val = parse_get_parameter_values_response(
552 self.acs.data_model,
553 message,
554 )
555 logger.debug('Received CPE parameter values: %s', str(name_to_val))
556 for name, val in name_to_val.items():
557 magma_val = self.acs.data_model.transform_for_magma(name, val)
558 self.acs.device_cfg.set_parameter(name, magma_val)
559 return AcsReadMsgResult(True, self.done_transition)
560
561 def state_description(self) -> str:
562 return 'Getting non-object parameters'
563
564
565class GetObjectParametersState(EnodebAcsState):
566 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
567 super().__init__()
568 self.acs = acs
569 self.done_transition = when_done
570
571 def get_msg(self, message: Any) -> AcsMsgAndTransition:
572 """ Respond with GetParameterValuesRequest """
573 names = get_object_params_to_get(
574 self.acs.desired_cfg,
575 self.acs.device_cfg,
576 self.acs.data_model,
577 )
578
579 # Generate the request
580 request = models.GetParameterValues()
581 request.ParameterNames = models.ParameterNames()
582 request.ParameterNames.arrayType = 'xsd:string[%d]' \
583 % len(names)
584 request.ParameterNames.string = []
585 for name in names:
586 path = self.acs.data_model.get_parameter(name).path
587 request.ParameterNames.string.append(path)
588
589 return AcsMsgAndTransition(request, self.done_transition)
590
591 def state_description(self) -> str:
592 return 'Getting object parameters'
593
594
595class WaitGetObjectParametersState(EnodebAcsState):
596 def __init__(
597 self,
598 acs: EnodebAcsStateMachine,
599 when_delete: str,
600 when_add: str,
601 when_set: str,
602 when_skip: str,
603 ):
604 super().__init__()
605 self.acs = acs
606 self.rm_obj_transition = when_delete
607 self.add_obj_transition = when_add
608 self.set_params_transition = when_set
609 self.skip_transition = when_skip
610
611 def read_msg(self, message: Any) -> AcsReadMsgResult:
612 """ Process GetParameterValuesResponse """
613 if not isinstance(message, models.GetParameterValuesResponse):
614 return AcsReadMsgResult(False, None)
615
616 path_to_val = {}
617 if hasattr(message.ParameterList, 'ParameterValueStruct') and \
618 message.ParameterList.ParameterValueStruct is not None:
619 for param_value_struct in message.ParameterList.ParameterValueStruct:
620 path_to_val[param_value_struct.Name] = \
621 param_value_struct.Value.Data
622 logger.debug('Received object parameters: %s', str(path_to_val))
623
624 # Number of PLMN objects reported can be incorrect. Let's count them
625 num_plmns = 0
626 obj_to_params = self.acs.data_model.get_numbered_param_names()
627 while True:
628 obj_name = ParameterName.PLMN_N % (num_plmns + 1)
629 if obj_name not in obj_to_params or len(obj_to_params[obj_name]) == 0:
630 logger.warning(
631 "eNB has PLMN %s but not defined in model",
632 obj_name,
633 )
634 break
635 param_name_list = obj_to_params[obj_name]
636 obj_path = self.acs.data_model.get_parameter(param_name_list[0]).path
637 if obj_path not in path_to_val:
638 break
639 if not self.acs.device_cfg.has_object(obj_name):
640 self.acs.device_cfg.add_object(obj_name)
641 num_plmns += 1
642 for name in param_name_list:
643 path = self.acs.data_model.get_parameter(name).path
644 value = path_to_val[path]
645 magma_val = \
646 self.acs.data_model.transform_for_magma(name, value)
647 self.acs.device_cfg.set_parameter_for_object(
648 name, magma_val,
649 obj_name,
650 )
651 num_plmns_reported = \
652 int(self.acs.device_cfg.get_parameter(ParameterName.NUM_PLMNS))
653 if num_plmns != num_plmns_reported:
654 logger.warning(
655 "eNB reported %d PLMNs but found %d",
656 num_plmns_reported, num_plmns,
657 )
658 self.acs.device_cfg.set_parameter(
659 ParameterName.NUM_PLMNS,
660 num_plmns,
661 )
662
663 # Now we can have the desired state
664 if self.acs.desired_cfg is None:
665 self.acs.desired_cfg = build_desired_config(
666 self.acs.mconfig,
667 self.acs.service_config,
668 self.acs.device_cfg,
669 self.acs.data_model,
670 self.acs.config_postprocessor,
671 )
672
673 if len(
674 get_all_objects_to_delete(
675 self.acs.desired_cfg,
676 self.acs.device_cfg,
677 ),
678 ) > 0:
679 return AcsReadMsgResult(True, self.rm_obj_transition)
680 elif len(
681 get_all_objects_to_add(
682 self.acs.desired_cfg,
683 self.acs.device_cfg,
684 ),
685 ) > 0:
686 return AcsReadMsgResult(True, self.add_obj_transition)
687 elif len(
688 get_all_param_values_to_set(
689 self.acs.desired_cfg,
690 self.acs.device_cfg,
691 self.acs.data_model,
692 ),
693 ) > 0:
694 return AcsReadMsgResult(True, self.set_params_transition)
695 return AcsReadMsgResult(True, self.skip_transition)
696
697 def state_description(self) -> str:
698 return 'Getting object parameters'
699
700
701class DeleteObjectsState(EnodebAcsState):
702 def __init__(
703 self,
704 acs: EnodebAcsStateMachine,
705 when_add: str,
706 when_skip: str,
707 ):
708 super().__init__()
709 self.acs = acs
710 self.deleted_param = None
711 self.add_obj_transition = when_add
712 self.skip_transition = when_skip
713
714 def get_msg(self, message: Any) -> AcsMsgAndTransition:
715 """
716 Send DeleteObject message to TR-069 and poll for response(s).
717 Input:
718 - Object name (string)
719 """
720 request = models.DeleteObject()
721 self.deleted_param = get_all_objects_to_delete(
722 self.acs.desired_cfg,
723 self.acs.device_cfg,
724 )[0]
725 request.ObjectName = \
726 self.acs.data_model.get_parameter(self.deleted_param).path
727 return AcsMsgAndTransition(request, None)
728
729 def read_msg(self, message: Any) -> AcsReadMsgResult:
730 """
731 Send DeleteObject message to TR-069 and poll for response(s).
732 Input:
733 - Object name (string)
734 """
735 if type(message) == models.DeleteObjectResponse:
736 if message.Status != 0:
737 raise Tr069Error(
738 'Received DeleteObjectResponse with '
739 'Status=%d' % message.Status,
740 )
741 elif type(message) == models.Fault:
742 raise Tr069Error(
743 'Received Fault in response to DeleteObject '
744 '(faultstring = %s)' % message.FaultString,
745 )
746 else:
747 return AcsReadMsgResult(False, None)
748
749 self.acs.device_cfg.delete_object(self.deleted_param)
750 obj_list_to_delete = get_all_objects_to_delete(
751 self.acs.desired_cfg,
752 self.acs.device_cfg,
753 )
754 if len(obj_list_to_delete) > 0:
755 return AcsReadMsgResult(True, None)
756 if len(
757 get_all_objects_to_add(
758 self.acs.desired_cfg,
759 self.acs.device_cfg,
760 ),
761 ) == 0:
762 return AcsReadMsgResult(True, self.skip_transition)
763 return AcsReadMsgResult(True, self.add_obj_transition)
764
765 def state_description(self) -> str:
766 return 'Deleting objects'
767
768
769class AddObjectsState(EnodebAcsState):
770 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
771 super().__init__()
772 self.acs = acs
773 self.done_transition = when_done
774 self.added_param = None
775
776 def get_msg(self, message: Any) -> AcsMsgAndTransition:
777 request = models.AddObject()
778 self.added_param = get_all_objects_to_add(
779 self.acs.desired_cfg,
780 self.acs.device_cfg,
781 )[0]
782 desired_param = self.acs.data_model.get_parameter(self.added_param)
783 desired_path = desired_param.path
784 path_parts = desired_path.split('.')
785 # If adding enumerated object, ie. XX.N. we should add it to the
786 # parent object XX. so strip the index
787 if len(path_parts) > 2 and \
788 path_parts[-1] == '' and path_parts[-2].isnumeric():
789 logger.debug('Stripping index from path=%s', desired_path)
790 desired_path = '.'.join(path_parts[:-2]) + '.'
791 request.ObjectName = desired_path
792 return AcsMsgAndTransition(request, None)
793
794 def read_msg(self, message: Any) -> AcsReadMsgResult:
795 if type(message) == models.AddObjectResponse:
796 if message.Status != 0:
797 raise Tr069Error(
798 'Received AddObjectResponse with '
799 'Status=%d' % message.Status,
800 )
801 elif type(message) == models.Fault:
802 raise Tr069Error(
803 'Received Fault in response to AddObject '
804 '(faultstring = %s)' % message.FaultString,
805 )
806 else:
807 return AcsReadMsgResult(False, None)
808 instance_n = message.InstanceNumber
809 self.acs.device_cfg.add_object(self.added_param % instance_n)
810 obj_list_to_add = get_all_objects_to_add(
811 self.acs.desired_cfg,
812 self.acs.device_cfg,
813 )
814 if len(obj_list_to_add) > 0:
815 return AcsReadMsgResult(True, None)
816 return AcsReadMsgResult(True, self.done_transition)
817
818 def state_description(self) -> str:
819 return 'Adding objects'
820
821
822class SetParameterValuesState(EnodebAcsState):
823 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
824 super().__init__()
825 self.acs = acs
826 self.done_transition = when_done
827
828 def get_msg(self, message: Any) -> AcsMsgAndTransition:
829 request = models.SetParameterValues()
830 request.ParameterList = models.ParameterValueList()
831 param_values = get_all_param_values_to_set(
832 self.acs.desired_cfg,
833 self.acs.device_cfg,
834 self.acs.data_model,
835 )
836 request.ParameterList.arrayType = 'cwmp:ParameterValueStruct[%d]' \
837 % len(param_values)
838 request.ParameterList.ParameterValueStruct = []
839 logger.debug(
840 'Sending TR069 request to set CPE parameter values: %s',
841 str(param_values),
842 )
843 # TODO: Match key response when we support having multiple outstanding
844 # calls.
845 if self.acs.has_version_key:
846 request.ParameterKey = models.ParameterKeyType()
847 request.ParameterKey.Data =\
848 "SetParameter-{:10.0f}".format(self.acs.parameter_version_key)
849 request.ParameterKey.type = 'xsd:string'
850
851 for name, value in param_values.items():
852 param_info = self.acs.data_model.get_parameter(name)
853 type_ = param_info.type
854 name_value = models.ParameterValueStruct()
855 name_value.Value = models.anySimpleType()
856 name_value.Name = param_info.path
857 enb_value = self.acs.data_model.transform_for_enb(name, value)
858 if type_ in ('int', 'unsignedInt'):
859 name_value.Value.type = 'xsd:%s' % type_
860 name_value.Value.Data = str(enb_value)
861 elif type_ == 'boolean':
862 # Boolean values have integral representations in spec
863 name_value.Value.type = 'xsd:boolean'
864 name_value.Value.Data = str(int(enb_value))
865 elif type_ == 'string':
866 name_value.Value.type = 'xsd:string'
867 name_value.Value.Data = str(enb_value)
868 else:
869 raise Tr069Error(
870 'Unsupported type for %s: %s' %
871 (name, type_),
872 )
873 if param_info.is_invasive:
874 self.acs.are_invasive_changes_applied = False
875 request.ParameterList.ParameterValueStruct.append(name_value)
876
877 return AcsMsgAndTransition(request, self.done_transition)
878
879 def state_description(self) -> str:
880 return 'Setting parameter values'
881
882
883class SetParameterValuesNotAdminState(EnodebAcsState):
884 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
885 super().__init__()
886 self.acs = acs
887 self.done_transition = when_done
888
889 def get_msg(self, message: Any) -> AcsMsgAndTransition:
890 request = models.SetParameterValues()
891 request.ParameterList = models.ParameterValueList()
892 param_values = get_all_param_values_to_set(
893 self.acs.desired_cfg,
894 self.acs.device_cfg,
895 self.acs.data_model,
896 exclude_admin=True,
897 )
898 request.ParameterList.arrayType = 'cwmp:ParameterValueStruct[%d]' \
899 % len(param_values)
900 request.ParameterList.ParameterValueStruct = []
901 logger.debug(
902 'Sending TR069 request to set CPE parameter values: %s',
903 str(param_values),
904 )
905 for name, value in param_values.items():
906 param_info = self.acs.data_model.get_parameter(name)
907 type_ = param_info.type
908 name_value = models.ParameterValueStruct()
909 name_value.Value = models.anySimpleType()
910 name_value.Name = param_info.path
911 enb_value = self.acs.data_model.transform_for_enb(name, value)
912 if type_ in ('int', 'unsignedInt'):
913 name_value.Value.type = 'xsd:%s' % type_
914 name_value.Value.Data = str(enb_value)
915 elif type_ == 'boolean':
916 # Boolean values have integral representations in spec
917 name_value.Value.type = 'xsd:boolean'
918 name_value.Value.Data = str(int(enb_value))
919 elif type_ == 'string':
920 name_value.Value.type = 'xsd:string'
921 name_value.Value.Data = str(enb_value)
922 else:
923 raise Tr069Error(
924 'Unsupported type for %s: %s' %
925 (name, type_),
926 )
927 if param_info.is_invasive:
928 self.acs.are_invasive_changes_applied = False
929 request.ParameterList.ParameterValueStruct.append(name_value)
930
931 return AcsMsgAndTransition(request, self.done_transition)
932
933 def state_description(self) -> str:
934 return 'Setting parameter values excluding Admin Enable'
935
936
937class WaitSetParameterValuesState(EnodebAcsState):
938 def __init__(
939 self,
940 acs: EnodebAcsStateMachine,
941 when_done: str,
942 when_apply_invasive: str,
943 status_non_zero_allowed: bool = False,
944 ):
945 super().__init__()
946 self.acs = acs
947 self.done_transition = when_done
948 self.apply_invasive_transition = when_apply_invasive
949 # Set Params can legally return zero and non zero status
950 # Per tr-196, if there are errors the method should return a fault.
951 # Make flag optional to compensate for existing radios returning non
952 # zero on error.
953 self.status_non_zero_allowed = status_non_zero_allowed
954
955 def read_msg(self, message: Any) -> AcsReadMsgResult:
956 if type(message) == models.SetParameterValuesResponse:
957 if not self.status_non_zero_allowed:
958 if message.Status != 0:
959 raise Tr069Error(
960 'Received SetParameterValuesResponse with '
961 'Status=%d' % message.Status,
962 )
963 self._mark_as_configured()
Wei-Yu Chen678f0a52021-12-21 13:50:52 +0800964
965 metrics.STAT_ENODEB_LAST_CONFIGURED.labels(
966 serial_number=self.acs.device_cfg.get_parameter("Serial number"),
967 ip_address=self.acs.device_cfg.get_parameter("ip_address"),
968 gps_lat=self.acs.device_cfg.get_parameter("GPS lat"),
969 gps_lon=self.acs.device_cfg.get_parameter("GPS long")
970 ).set(int(time.time()))
971
Wei-Yu Chen49950b92021-11-08 19:19:18 +0800972 if not self.acs.are_invasive_changes_applied:
973 return AcsReadMsgResult(True, self.apply_invasive_transition)
974 return AcsReadMsgResult(True, self.done_transition)
975 elif type(message) == models.Fault:
976 logger.error(
977 'Received Fault in response to SetParameterValues, '
978 'Code (%s), Message (%s)', message.FaultCode,
979 message.FaultString,
980 )
981 if message.SetParameterValuesFault is not None:
982 for fault in message.SetParameterValuesFault:
983 logger.error(
984 'SetParameterValuesFault Param: %s, '
985 'Code: %s, String: %s', fault.ParameterName,
986 fault.FaultCode, fault.FaultString,
987 )
988 return AcsReadMsgResult(False, None)
989
990 def _mark_as_configured(self) -> None:
991 """
992 A successful attempt at setting parameter values means that we need to
993 update what we think the eNB's configuration is to match what we just
994 set the parameter values to.
995 """
996 # Values of parameters
997 name_to_val = get_param_values_to_set(
998 self.acs.desired_cfg,
999 self.acs.device_cfg,
1000 self.acs.data_model,
1001 )
1002 for name, val in name_to_val.items():
1003 magma_val = self.acs.data_model.transform_for_magma(name, val)
1004 self.acs.device_cfg.set_parameter(name, magma_val)
1005
1006 # Values of object parameters
1007 obj_to_name_to_val = get_obj_param_values_to_set(
1008 self.acs.desired_cfg,
1009 self.acs.device_cfg,
1010 self.acs.data_model,
1011 )
1012 for obj_name, name_to_val in obj_to_name_to_val.items():
1013 for name, val in name_to_val.items():
1014 logger.debug(
1015 'Set obj: %s, name: %s, val: %s', str(obj_name),
1016 str(name), str(val),
1017 )
1018 magma_val = self.acs.data_model.transform_for_magma(name, val)
1019 self.acs.device_cfg.set_parameter_for_object(
1020 name, magma_val,
1021 obj_name,
1022 )
1023 logger.info('Successfully configured CPE parameters!')
1024
1025 def state_description(self) -> str:
1026 return 'Setting parameter values'
1027
1028
1029class EndSessionState(EnodebAcsState):
1030 """ To end a TR-069 session, send an empty HTTP response """
1031
1032 def __init__(self, acs: EnodebAcsStateMachine):
1033 super().__init__()
1034 self.acs = acs
1035
1036 def read_msg(self, message: Any) -> AcsReadMsgResult:
1037 """
1038 No message is expected after enodebd sends the eNodeB
1039 an empty HTTP response.
1040
1041 If a device sends an empty HTTP request, we can just
1042 ignore it and send another empty response.
1043 """
1044 if isinstance(message, models.DummyInput):
1045 return AcsReadMsgResult(True, None)
1046 return AcsReadMsgResult(False, None)
1047
1048 def get_msg(self, message: Any) -> AcsMsgAndTransition:
1049 request = models.DummyInput()
1050 return AcsMsgAndTransition(request, None)
1051
1052 def state_description(self) -> str:
1053 return 'Completed provisioning eNB. Awaiting new Inform.'
1054
1055
1056class EnbSendRebootState(EnodebAcsState):
1057 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
1058 super().__init__()
1059 self.acs = acs
1060 self.done_transition = when_done
1061 self.prev_msg_was_inform = False
1062
1063 def read_msg(self, message: Any) -> AcsReadMsgResult:
1064 """
1065 This state can be transitioned into through user command.
1066 All messages received by enodebd will be ignored in this state.
1067 """
1068 if self.prev_msg_was_inform \
1069 and not isinstance(message, models.DummyInput):
1070 return AcsReadMsgResult(False, None)
1071 elif isinstance(message, models.Inform):
1072 self.prev_msg_was_inform = True
1073 process_inform_message(
1074 message, self.acs.data_model,
1075 self.acs.device_cfg,
1076 )
1077 return AcsReadMsgResult(True, None)
1078 self.prev_msg_was_inform = False
1079 return AcsReadMsgResult(True, None)
1080
1081 def get_msg(self, message: Any) -> AcsMsgAndTransition:
1082 if self.prev_msg_was_inform:
1083 response = models.InformResponse()
1084 # Set maxEnvelopes to 1, as per TR-069 spec
1085 response.MaxEnvelopes = 1
1086 return AcsMsgAndTransition(response, None)
1087 logger.info('Sending reboot request to eNB')
1088 request = models.Reboot()
1089 request.CommandKey = ''
1090 self.acs.are_invasive_changes_applied = True
1091 return AcsMsgAndTransition(request, self.done_transition)
1092
1093 def state_description(self) -> str:
1094 return 'Rebooting eNB'
1095
1096
1097class SendRebootState(EnodebAcsState):
1098 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
1099 super().__init__()
1100 self.acs = acs
1101 self.done_transition = when_done
1102 self.prev_msg_was_inform = False
1103
1104 def read_msg(self, message: Any) -> AcsReadMsgResult:
1105 """
1106 This state can be transitioned into through user command.
1107 All messages received by enodebd will be ignored in this state.
1108 """
1109 if self.prev_msg_was_inform \
1110 and not isinstance(message, models.DummyInput):
1111 return AcsReadMsgResult(False, None)
1112 elif isinstance(message, models.Inform):
1113 self.prev_msg_was_inform = True
1114 process_inform_message(
1115 message, self.acs.data_model,
1116 self.acs.device_cfg,
1117 )
1118 return AcsReadMsgResult(True, None)
1119 self.prev_msg_was_inform = False
1120 return AcsReadMsgResult(True, None)
1121
1122 def get_msg(self, message: Any) -> AcsMsgAndTransition:
1123 if self.prev_msg_was_inform:
1124 response = models.InformResponse()
1125 # Set maxEnvelopes to 1, as per TR-069 spec
1126 response.MaxEnvelopes = 1
1127 return AcsMsgAndTransition(response, None)
1128 logger.info('Sending reboot request to eNB')
1129 request = models.Reboot()
1130 request.CommandKey = ''
1131 return AcsMsgAndTransition(request, self.done_transition)
1132
1133 def state_description(self) -> str:
1134 return 'Rebooting eNB'
1135
1136
1137class WaitRebootResponseState(EnodebAcsState):
1138 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
1139 super().__init__()
1140 self.acs = acs
1141 self.done_transition = when_done
1142
1143 def read_msg(self, message: Any) -> AcsReadMsgResult:
1144 if not isinstance(message, models.RebootResponse):
1145 return AcsReadMsgResult(False, None)
1146 return AcsReadMsgResult(True, None)
1147
1148 def get_msg(self, message: Any) -> AcsMsgAndTransition:
1149 """ Reply with empty message """
1150 return AcsMsgAndTransition(models.DummyInput(), self.done_transition)
1151
1152 def state_description(self) -> str:
1153 return 'Rebooting eNB'
1154
1155
1156class WaitInformMRebootState(EnodebAcsState):
1157 """
1158 After sending a reboot request, we expect an Inform request with a
1159 specific 'inform event code'
1160 """
1161
1162 # Time to wait for eNodeB reboot. The measured time
1163 # (on BaiCells indoor eNodeB)
1164 # is ~110secs, so add healthy padding on top of this.
1165 REBOOT_TIMEOUT = 300 # In seconds
1166 # We expect that the Inform we receive tells us the eNB has rebooted
1167 INFORM_EVENT_CODE = 'M Reboot'
1168
1169 def __init__(
1170 self,
1171 acs: EnodebAcsStateMachine,
1172 when_done: str,
1173 when_timeout: str,
1174 ):
1175 super().__init__()
1176 self.acs = acs
1177 self.done_transition = when_done
1178 self.timeout_transition = when_timeout
1179 self.timeout_timer = None
1180 self.timer_handle = None
1181
1182 def enter(self):
1183 self.timeout_timer = StateMachineTimer(self.REBOOT_TIMEOUT)
1184
1185 def check_timer() -> None:
1186 if self.timeout_timer.is_done():
1187 self.acs.transition(self.timeout_transition)
1188 raise Tr069Error(
1189 'Did not receive Inform response after '
1190 'rebooting',
1191 )
1192
1193 self.timer_handle = \
1194 self.acs.event_loop.call_later(
1195 self.REBOOT_TIMEOUT,
1196 check_timer,
1197 )
1198
1199 def exit(self):
1200 self.timer_handle.cancel()
1201 self.timeout_timer = None
1202
1203 def read_msg(self, message: Any) -> AcsReadMsgResult:
1204 if not isinstance(message, models.Inform):
1205 return AcsReadMsgResult(False, None)
1206 if not does_inform_have_event(message, self.INFORM_EVENT_CODE):
1207 raise Tr069Error(
1208 'Did not receive M Reboot event code in '
1209 'Inform',
1210 )
1211 process_inform_message(
1212 message, self.acs.data_model,
1213 self.acs.device_cfg,
1214 )
1215 return AcsReadMsgResult(True, self.done_transition)
1216
1217 def state_description(self) -> str:
1218 return 'Waiting for M Reboot code from Inform'
1219
1220
1221class WaitRebootDelayState(EnodebAcsState):
1222 """
1223 After receiving the Inform notifying us that the eNodeB has successfully
1224 rebooted, wait a short duration to prevent unspecified race conditions
1225 that may occur w.r.t reboot
1226 """
1227
1228 # Short delay timer to prevent race conditions w.r.t. reboot
1229 SHORT_CONFIG_DELAY = 10
1230
1231 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
1232 super().__init__()
1233 self.acs = acs
1234 self.done_transition = when_done
1235 self.config_timer = None
1236 self.timer_handle = None
1237
1238 def enter(self):
1239 self.config_timer = StateMachineTimer(self.SHORT_CONFIG_DELAY)
1240
1241 def check_timer() -> None:
1242 if self.config_timer.is_done():
1243 self.acs.transition(self.done_transition)
1244
1245 self.timer_handle = \
1246 self.acs.event_loop.call_later(
1247 self.SHORT_CONFIG_DELAY,
1248 check_timer,
1249 )
1250
1251 def exit(self):
1252 self.timer_handle.cancel()
1253 self.config_timer = None
1254
1255 def read_msg(self, message: Any) -> AcsReadMsgResult:
1256 return AcsReadMsgResult(True, None)
1257
1258 def get_msg(self, message: Any) -> AcsMsgAndTransition:
1259 return AcsMsgAndTransition(models.DummyInput(), None)
1260
1261 def state_description(self) -> str:
1262 return 'Waiting after eNB reboot to prevent race conditions'
1263
1264
Wei-Yu Chen678f0a52021-12-21 13:50:52 +08001265class DownloadState(EnodebAcsState):
1266 """
1267 The eNB handler will enter this state when firmware version is older than desired version.
1268 """
1269
1270 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
1271 super().__init__()
1272 self.acs = acs
1273 self.done_transition = when_done
1274
1275 def get_msg(self, message: Any) -> AcsMsgAndTransition:
1276
1277 print("ACS Device CFG")
1278 print(self.acs.device_cfg._param_to_value)
1279
1280 request = models.Download()
1281 request.CommandKey = "20220206215200"
1282 request.FileType = "1 Firmware Upgrade Image"
Wei-Yu Chenad55cb82022-02-15 20:07:01 +08001283 request.URL = "http://10.128.250.131/firmware/Qproject_TEST3918_2102241222.ffw"
Wei-Yu Chen678f0a52021-12-21 13:50:52 +08001284 request.Username = ""
1285 request.Password = ""
1286 request.FileSize = 57208579
1287 request.TargetFileName = "Qproject_TEST3918_2102241222.ffw"
1288 request.DelaySeconds = 0
1289 request.SuccessURL = ""
1290 request.FailureURL = ""
1291 return AcsMsgAndTransition(request, self.done_transition)
1292
1293 def state_description(self) -> str:
1294 return 'Upgrade the firmware the desired version'
1295
1296class WaitDownloadResponseState(EnodebAcsState):
1297 """
1298 The eNB handler will enter this state after the Download command sent.
1299 """
1300
1301 def __init__(self, acs: EnodebAcsStateMachine, when_done: str):
1302 super().__init__()
1303 self.acs = acs
1304 self.done_transition = when_done
1305
1306 def read_msg(self, message: Any) -> AcsReadMsgResult:
1307 if not isinstance(message, models.DownloadResponse):
1308 return AcsReadMsgResult(False, None)
1309 return AcsReadMsgResult(True, None)
1310
1311 def get_msg(self, message: Any) -> AcsMsgAndTransition:
1312 """ Reply with empty message """
1313 logger.info("Received Download Response from eNodeB")
1314 return AcsMsgAndTransition(models.DummyInput(), self.done_transition)
1315
1316 def state_description(self) -> str:
1317 return "Wait DownloadResponse message"
1318
1319class WaitInformTransferCompleteState(EnodebAcsState):
1320 """
1321 The eNB handler will enter this state after firmware upgraded and rebooted
1322 """
1323
1324 REBOOT_TIMEOUT = 300 # In seconds
1325 INFORM_EVENT_CODE = "7 TRANSFER COMPLETE"
1326 PREIODIC_EVENT_CODE = "2 PERIODIC"
1327
1328 def __init__(self, acs: EnodebAcsStateMachine, when_done: str, when_periodic: str, when_timeout: str):
1329 super().__init__()
1330 self.acs = acs
1331 self.done_transition = when_done
1332 self.periodic_update_transition = when_periodic
1333 self.timeout_transition = when_timeout
1334 self.timeout_timer = None
1335 self.timer_handle = None
1336
1337 def enter(self):
1338 print("Get into the TransferComplete State")
1339 self.timeout_timer = StateMachineTimer(self.REBOOT_TIMEOUT)
1340
1341 def check_timer() -> None:
1342 if self.timeout_timer.is_done():
1343 self.acs.transition(self.timeout_transition)
1344 raise Tr069Error("Didn't receive Inform response after rebooting")
1345
1346 self.timer_handle = self.acs.event_loop.call_later(
1347 self.REBOOT_TIMEOUT,
1348 check_timer,
1349 )
1350
1351 def exit(self):
1352 self.timer_handle.cancel()
1353 self.timeout_timer = None
1354
1355 def get_msg(self, message: Any) -> AcsMsgAndTransition:
1356 return AcsMsgAndTransition(models.DummyInput(), None)
1357
1358 def read_msg(self, message: Any) -> AcsReadMsgResult:
1359 if not isinstance(message, models.Inform):
1360 return AcsReadMsgResult(False, None)
1361 if does_inform_have_event(message, self.PREIODIC_EVENT_CODE):
1362 logger.info("Receive Periodic update from enodeb")
1363 return AcsReadMsgResult(True, self.periodic_update_transition)
1364 if does_inform_have_event(message, self.INFORM_EVENT_CODE):
1365 logger.info("Receive Transfer complete")
1366 return AcsReadMsgResult(True, self.done_transition)
1367
1368 # Unhandled situation
1369 return AcsReadMsgResult(False, None)
1370
1371 def state_description(self) -> str:
1372 return "Wait DownloadResponse message"
1373
Wei-Yu Chen49950b92021-11-08 19:19:18 +08001374class ErrorState(EnodebAcsState):
1375 """
1376 The eNB handler will enter this state when an unhandled Fault is received.
1377
1378 If the inform_transition_target constructor parameter is non-null, this
1379 state will attempt to autoremediate by transitioning to the specified
1380 target state when an Inform is received.
1381 """
1382
1383 def __init__(
1384 self, acs: EnodebAcsStateMachine,
1385 inform_transition_target: Optional[str] = None,
1386 ):
1387 super().__init__()
1388 self.acs = acs
1389 self.inform_transition_target = inform_transition_target
1390
1391 def read_msg(self, message: Any) -> AcsReadMsgResult:
1392 return AcsReadMsgResult(True, None)
1393
1394 def get_msg(self, message: Any) -> AcsMsgAndTransition:
1395 if not self.inform_transition_target:
1396 return AcsMsgAndTransition(models.DummyInput(), None)
1397
1398 if isinstance(message, models.Inform):
1399 return AcsMsgAndTransition(
1400 models.DummyInput(),
1401 self.inform_transition_target,
1402 )
1403 return AcsMsgAndTransition(models.DummyInput(), None)
1404
1405 def state_description(self) -> str:
1406 return 'Error state - awaiting manual restart of enodebd service or ' \
Wei-Yu Chen678f0a52021-12-21 13:50:52 +08001407 'an Inform to be received from the eNB'