Commit e8b879f7 authored by Yann Garcia's avatar Yann Garcia
Browse files

Add plugin for MEC support in ACME; Update example README; Bug fixed in MEC 033/046

parent 4251290b
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
Demo9 is a MEC application to illustrate the joint usage of the ETSI MEC platform and oneM2M platform.
Demo9 is a MEC application to illustrate the joint usage of the ETSI MEC platform and oneM2M CSE.

# How to use it

+1 −1
Original line number Diff line number Diff line
@@ -111,4 +111,4 @@ workdir="/usr/src/app/ACME-oneM2M-CSE"
cd "$workdir" || { echo "Directory $workdir not found"; exit 1; }
envsubst < acme.ini.in > acme.ini
cat acme.ini
python3 -m acme
python3 -m acmecse
+1 −1
Original line number Diff line number Diff line
@@ -50,7 +50,7 @@ RUN echo "meep-acme-mn-cse" > /etc/hostname \

WORKDIR /usr/src/app

RUN git clone --branch development https://github.com/ankraft/ACME-oneM2M-CSE.git ACME-oneM2M-CSE
RUN git clone https://github.com/ankraft/ACME-oneM2M-CSE.git ACME-oneM2M-CSE

WORKDIR /usr/src/app/ACME-oneM2M-CSE

+2 −2
Original line number Diff line number Diff line
@@ -44,8 +44,8 @@ allowedCSROriginators=/*

[cse.registrar]
INCSEcseID=/$REMOTE_CSE_ID
address=https://${basic.config:registrarCseHost}:${basic.config:registrarCsePort}/
root=$REMOTE_SVC_PATH
;address=https://${basic.config:registrarCseHost}:${basic.config:registrarCsePort}/
;root=$REMOTE_SVC_PATH

[textui]
startWithTUI=false
+0 −507
Original line number Diff line number Diff line
#
#	CSE.py
#
#	(c) 2020 by Andreas Kraft
#	License: BSD 3-Clause License. See the LICENSE file for further details.
#
#	Container that holds references to instances of various managing entities.
#

"""	This module implements various functions for CSE startip, running, resetting, shutdown etc.
	It also provides various global variable that hold fixed configuration values.
	In addition is holds pointers to the various runtime instance of CSE modules, packages etc.
"""

from __future__ import annotations
from typing import Dict, Any, cast

import atexit, argparse, sys, platform, os, signal, platform
from threading import Lock

from ..helpers.BackgroundWorker import BackgroundWorkerPool
from ..etc.Constants import Constants as C, RuntimeConstants as RC
from ..etc.DateUtils import waitFor
from ..etc.Utils import runsInIPython
from ..etc.Types import CSEStatus, LogLevel
from ..etc.Constants import RuntimeConstants as RC
from ..etc.ResponseStatusCodes import ResponseException
from ..services.ActionManager import ActionManager
from ..runtime.Configuration import Configuration
from ..runtime.Console import Console


from ..services.Dispatcher import Dispatcher
from ..services.RequestManager import RequestManager
from .EventManager import EventManager
from ..services.GroupManager import GroupManager
from ..runtime.Importer import Importer
from ..services.LocationManager import LocationManager
from ..services.NotificationManager import NotificationManager
from ..runtime.PluginManager import PluginManager
from ..services.RegistrationManager import RegistrationManager
from ..services.RemoteCSEManager import RemoteCSEManager
from ..runtime.ScriptManager import ScriptManager
from ..services.SecurityManager import SecurityManager
from ..services.SemanticManager import SemanticManager
from ..runtime.Statistics import Statistics
from ..runtime.Storage import Storage
from ..runtime.TextUI import TextUI
from ..services.TimeManager import TimeManager
from ..services.TimeSeriesManager import TimeSeriesManager
from ..services.Validator import Validator
from ..protocols.HttpServer import HttpServer
from ..protocols.CoAPServer import CoAPServer
from ..protocols.MQTTClient import MQTTClient
from ..protocols.WebSocketServer import WebSocketServer
from ..services.AnnouncementManager import AnnouncementManager
from ..runtime.Logging import Logging as L
from ..protocols.MECClient import MECClient

##############################################################################

# singleton main components. These variables will hold all the various manager
# components that are used throughout the CSE implementation.

action:ActionManager = None
"""	Runtime instance of the `ActionManager`. """

announce:AnnouncementManager = None
"""	Runtime instance of the `AnnouncementManager`. """

coapServer:CoAPServer = None
"""	Runtime instance of the `CoAPServer`. """

console:Console = None
""" Runtime instance of the `Console`. """

dispatcher:Dispatcher = None
"""	Runtime instance of the `Dispatcher`. """

event:EventManager = None
"""	Runtime instance of the `EventManager`. """

groupResource:GroupManager = None
"""	Runtime instance of the `GroupManager`. """

httpServer:HttpServer = None
"""	Runtime instance of the `HttpServer`. """

importer:Importer = None
"""	Runtime instance of the `Importer`. """

location:LocationManager = None
"""	Runtime instance of the `LocationManager`. """

mqttClient:MQTTClient = None
"""	Runtime instance of the `MQTTClient`. """

notification:NotificationManager = None
"""	Runtime instance of the `NotificationManager`. """

pluginManager:PluginManager = None
"""	Runtime instance of the `PluginManager`. """

registration:RegistrationManager = None
"""	Runtime instance of the `RegistrationManager`. """

remote:RemoteCSEManager = None
"""	Runtime instance of the `RemoteCSEManager`. """

request:RequestManager = None
"""	Runtime instance of the `RequestManager`. """

script:ScriptManager = None
"""	Runtime instance of the `ScriptManager`. """

security:SecurityManager = None
"""	Runtime instance of the `SecurityManager`. """

semantic:SemanticManager = None
"""	Runtime instance of the `SemanticManager`. """

statistics:Statistics = None
"""	Runtime instance of the `Statistics`. """

storage:Storage = None
"""	Runtime instance of the `Storage`. """

textUI:TextUI = None
"""	Runtime instance of the `TextUI`. """

time:TimeManager = None
"""	Runtime instance of the `TimeManager`. """

timeSeries:TimeSeriesManager = None
"""	Runtime instance of the `TimeSeriesManager`. """

validator:Validator = None
"""	Runtime instance of the `Validator`. """

webSocketServer:WebSocketServer	= None
"""	Runtime instance of the `WebSocketServer`. """


mecClient:MECClient = None
"""	Runtime instance of the `MECClient`. """


# Global variables to hold various (configuation) values.

_cseResetLock = Lock()
""" Internal CSE's lock when resetting. """

##############################################################################


def startup(args:argparse.Namespace, **kwargs:Dict[str, Any]) -> bool:
	"""	Startup of the CSE. Initialization of various global variables, creating and initializing of manager instances etc.
	
		Args:
			args: Startup command line arguments.
			kwargs: Optional, additional keyword arguments which are added as attributes to the *args* object.
		Return:
			False if the CSE couldn't initialized and started. 
	"""
	global action, announce, coapServer, console, dispatcher, event, groupResource, httpServer, importer, location, mqttClient, mecClient
	global notification, pluginManager, registration, remote, request, script, security, semantic
	global statistics, storage, textUI, time, timeSeries, validator, webSocketServer

	# Set status
	RC.cseStatus = CSEStatus.STARTING

	# Handle command line arguments and load the configuration
	if not args:
		args = argparse.Namespace()		# In case args is None create a new args object and populate it
		args.configfile	= None
		args.resetdb	= False
		args.loglevel	= None
		args.headless	= False
		for key, value in kwargs.items():
			args.__setattr__(key, value)

	event = EventManager()					# Initialize the event manager before anything else

	if not Configuration.init(args):
		RC.cseStatus = CSEStatus.STOPPED
		return False

	#
	# init Logging
	#
	L.init()
	L.queueOff()				# No queuing of log messages during startup
	L.log('Starting CSE')
	L.log(f'CSE-Type: {RC.cseType.name}')
	if args.printconfig:
		for l in Configuration.print().split('\n'):
			L.log(l)
	
	# set the logger for the backgroundWorkers. Add an offset to compensate for
	# this and other redirect functions to determine the correct file / linenumber
	# in the log output
	BackgroundWorkerPool.setLogger(lambda l,m: L.logWithLevel(l, m, stackOffset = 2))
	BackgroundWorkerPool.setJobBalance(	balanceTarget = Configuration.cse_operation_jobs_balanceTarget,
										balanceLatency = Configuration.cse_operation_jobs_balanceLatency,
										balanceReduceFactor = Configuration.cse_operation_jobs_balanceReduceFactor)

	try:
		textUI = TextUI()						# Start the textUI
		console = Console()						# Start the console
		storage = Storage()						# Initialize the resource storage

		importer = Importer()					# Initialize the importer
		importer.importResourcePolicies()		# Before initializing other components, import the resource policies

		statistics = Statistics()				# Initialize the statistics system
		registration = RegistrationManager()	# Initialize the registration manager
		validator = Validator()					# Initialize the resource validator
		dispatcher = Dispatcher()				# Initialize the resource dispatcher
		request = RequestManager()				# Initialize the request manager
		security = SecurityManager()			# Initialize the security manager
		httpServer = HttpServer() if not httpServer else httpServer		# Initialize the HTTP server
		coapServer = CoAPServer()				# Initialize the CoAP server
		mqttClient = MQTTClient()				# Initialize the MQTT client
		webSocketServer = WebSocketServer()		# Initialize the WebSocket server
		notification = NotificationManager()	# Initialize the notification manager
		groupResource = GroupManager()					# Initialize the group manager
		timeSeries = TimeSeriesManager()		# Initialize the timeSeries manager
		remote = RemoteCSEManager()				# Initialize the remote CSE manager
		announce = AnnouncementManager()		# Initialize the announcement manager
		semantic = SemanticManager()			# Initialize the semantic manager
		location = LocationManager()			# Initialize the location manager
		time = TimeManager()					# Initialize the time mamanger
		script = ScriptManager()				# Initialize the script manager
		action = ActionManager()				# Initialize the action manager

		# Import attribute, flexContainer and enum policies, and configuration documentation
		#
		# After this, the CSE reads the scripts from the default and runtime init directories.
		# It also runs the init script, if there is one. 
		#


		if Configuration.mec_enable:
				L.log('Initializing MEC client')
				if Configuration.mqtt_websocket_enable:
					mecClient = MECClient(Configuration.http_address, Configuration.http_port, Configuration.cse_resourceID, Configuration.cse_external_ip, Configuration.cse_external_port, Configuration.mqtt_websocket_enable, Configuration.mqtt_address, Configuration.mqtt_websocket_port, Configuration.fullAddress, Configuration.mec_app_instance_id, Configuration.mqtt_websocket_path)	
				else:
					mecClient = MECClient(Configuration.http_address, Configuration.http_port, Configuration.cse_resourceID, Configuration.cse_external_ip, Configuration.cse_external_port, Configuration.mqtt_websocket_enable, Configuration.mqtt_address, Configuration.mqtt_port, Configuration.fullAddress, Configuration.mec_app_instance_id)
				if mecClient is None:
					Configuration.mec_enable = False

		# When this fails, we cannot continue with the CSE startup
		if not importer.importPolicies() or not importer.importScripts():
			RC.cseStatus = CSEStatus.STOPPED
			return False
		
		# Initialize the plugin manager
		# This loads, configures and runs the plugins as well
		pluginManager = PluginManager()	


		# Start the HTTP server
		if not httpServer.run(): 						# This does return (!)
			L.logErr('Terminating', showStackTrace = False)
			RC.cseStatus = CSEStatus.STOPPED
			return False 					

		# Start the CoAP server
		if not coapServer.run():					# This does return
			L.logErr('Terminating', showStackTrace = False)
			RC.cseStatus = CSEStatus.STOPPED
			return False

		# Start the MQTT client
		if not mqttClient.run():				# This does return
			L.logErr('Terminating', showStackTrace = False)
			RC.cseStatus = CSEStatus.STOPPED
			return False 

		# Start the WebSocket server
		if not webSocketServer.run():			# This does return
			L.logErr('Terminating', showStackTrace = False)
			RC.cseStatus = CSEStatus.STOPPED
			return False
	
	except ResponseException as e:
		L.logErr(f'Error during startup: {e.dbg}')
		RC.cseStatus = CSEStatus.STOPPED
		return False
	except KeyError as e:
		L.logErr(f'Error during startup: {e}')
		RC.cseStatus = CSEStatus.STOPPED
		return False

	except Exception as e:
		L.logErr(f'Error during startup: {e}', exc=e)
		RC.cseStatus = CSEStatus.STOPPED
		return False

	# Enable log queuing
	L.queueOn()	


	# Give the CSE a moment (2s) to experience fatal errors before printing the start message

	def _startUpFinished() -> None:
		"""	Internal function to print the CSE startup message after a delay
		"""
		RC.cseStatus = CSEStatus.RUNNING
		# Send an event that the CSE startup finished
		event.cseStartup()	# type: ignore

		L.console('CSE started')
		L.log('CSE started')

	BackgroundWorkerPool.newActor(_startUpFinished, delay = C.cseStartupDelay if RC.isHeadless else C.cseStartupDelay / 2.0, name = 'Delayed_startup_message' ).start()
	
	return True


def shutdown() -> None:
	"""	Gracefully shutdown the CSE programmatically. This will end the main console loop
		to terminate.

		The actual shutdown happens in the _shutdown() method.
	"""
	if RC.cseStatus in [ CSEStatus.SHUTTINGDOWN, CSEStatus.STOPPED ]:
		return
	
	# indicating the shutting down status. When running in another environment the
	# atexit-handler might not be called. Therefore, we need to set it here
	if RC.cseStatus != CSEStatus.SHUTTINGDOWNRESTART:	# only set this if we are not restarting
		RC.cseStatus = CSEStatus.SHUTTINGDOWN
	if console:
		console.stop()				# This will end the main run loop.
	
	if mecClient:
		mecClient.deregisterFromMec()

	if runsInIPython():
		L.console('CSE shut down', nlb = True)


@atexit.register
def _shutdown() -> None:
	"""	Shutdown the CSE, e.g. when receiving a keyboard interrupt or at the end of the programm run.
	"""
	if RC.cseStatus not in [CSEStatus.RUNNING, CSEStatus.SHUTTINGDOWNRESTART]:
		return
	L.console('CSE shutting down now', nlb=True)
	
	# The status STOPPINGRESTART is used to indicate that the CSE is shutting down to restart.
	# This is a normal shutdown but in the end the CSE process will return with a special exit code
	# to indicate that the CSE is restarting. This code is 82 (ASCII code for 'R').
	_cseStatus = RC.cseStatus	
	RC.cseStatus = CSEStatus.SHUTTINGDOWN
	L.queueOff()
	L.isInfo and L.log('CSE shutting down')
	if event:	# send shutdown event
		event.cseShutdown() 	# type: ignore
	
	# shutdown the services
	pluginManager and pluginManager.shutdown()

	textUI and textUI.shutdown()
	console and console.shutdown()
	time and time.shutdown()
	location and location.shutdown()
	semantic and semantic.shutdown()
	remote and remote.shutdown()
	coapServer and coapServer.shutdown()
	webSocketServer and webSocketServer.shutdown()
	mqttClient and mqttClient.shutdown()
	httpServer and httpServer.shutdown()
	script and script.shutdown()
	announce and announce.shutdown()
	timeSeries and timeSeries.shutdown()
	groupResource and groupResource.shutdown()
	notification and notification.shutdown()
	request and request.shutdown()
	dispatcher and dispatcher.shutdown()
	security and security.shutdown()
	validator and validator.shutdown()
	registration and registration.shutdown()
	statistics and statistics.shutdown()
	event and event.shutdown()
	storage  and storage.shutdown()

	mecClient and mecClient.shutdown()

	L.isInfo and L.log('CSE shut down')
	L.console('CSE shut down', nlb = True)

	L.finit()
	RC.cseStatus = CSEStatus.STOPPED

	# If the CSE is stopping to restart, we exit with a special exit code
	if _cseStatus == CSEStatus.SHUTTINGDOWNRESTART:
		os._exit(82) 

def forceShutdown() -> None:
	"""	Force shutdown the CSE. 
	
		This is different for different platforms. On Windows, we send a SIGINT to the process,
		while on other platforms we raise a SIGINT signal. This is to ensure that the CSE can
		shutdown gracefully, even if the main thread is blocked or busy.

		This function might not return, e.g. when running under Windows, where the process is killed.
	"""	
	_platform = platform.system()
	L.isDebug and L.logDebug(f'Forcing CSE shutdown (Platform: {_platform})')

	if textUI and textUI.tuiApp:	# Shutdown the TextUI first
		textUI.shutdown()	
		import time as _time
		_time.sleep(1)	 			# Give the TextUI a moment to shutdown

	# Platform specific shutdown
	# On Windows, we send a SIGINT to the process, which will be caught by the main thread
	match _platform:
		case 'Windows':
			_shutdown()
			os.kill(os.getpid(), signal.SIGINT)
		case _:
			signal.raise_signal(signal.SIGINT)	# raise SIGINT to shutdown the CSE


def resetCSE() -> None:
	""" Reset the CSE: Clear databases and import the resources again.
	"""
	with _cseResetLock:
		RC.cseStatus = CSEStatus.RESETTING
		L.isWarn and L.logWarn('Resetting CSE started')
		L.enableScreenLogging = Configuration.logging_enableScreenLogging	# Set screen logging to the originally configured values

		L.setLogLevel(cast(LogLevel, Configuration.logging_level))
		L.queueOff()	# Disable log queuing for restart
		
		httpServer.pause()
		mqttClient.pause()
		webSocketServer.shutdown()	# WS Server needs to be shutdown to close connections
		coapServer.pause()

		mecClient.pause()


		storage.purge()

		# The following event is executed synchronously to give every component
		# a chance to finish
		event.cseReset()	# type: ignore [attr-defined]

		# We only import policies, documentation and scripts during restart
		# But we don't import the resource policies again.
		if not importer.importPolicies() or not importer.importScripts():
			textUI and textUI.shutdown()
			L.logErr('Error during import')
			sys.exit()	# what else can we do?
		remote.restart()

		coapServer.unpause()
		webSocketServer.run()	# WS Server restart
		mqttClient.unpause()
		httpServer.unpause()
		mecClient.unpause()

		# Enable log queuing again
		L.queueOn()

		# Send restarted event
		event.cseRestarted()	# type: ignore [attr-defined]   

		RC.cseStatus = CSEStatus.RUNNING
		L.isWarn and L.logWarn('Resetting CSE finished')


def restartCSE() -> None:
	"""	Restart the CSE. This is a convenience function that calls the shutdown() function.
	"""
	if RC.cseStatus != CSEStatus.RUNNING:
		L.logErr('CSE is not running, cannot restart')
		return
	L.isWarn and L._log(LogLevel.WARNING, 'Restarting CSE', immediate=True)
	console.stop()
	_shutdown()
	RC.cseStatus = CSEStatus.SHUTTINGDOWNRESTART


def run() -> None:
	"""	Run the CSE.

		Raises:
			TimeoutError: If the CSE does not start within the specified time.
	"""

	L.logDebug(f'MEC enable: {Configuration.mec_enable}')
	if Configuration.mec_enable:
		mecClient.confirm_readyToMec()
		mecClient.post_mec_service()
		mecClient.registerToMec()

	if waitFor(C.cseStartupDelay * 3, lambda: RC.cseStatus==CSEStatus.RUNNING):
		console.run()
	else:
		raise TimeoutError(L.logErr(f'CSE did not start within {C.cseStartupDelay * 3} seconds'))

Loading