From 35c48e67a30127b2522322e6928f9606696b06ae Mon Sep 17 00:00:00 2001 From: "Blake J. Harnden" Date: Thu, 3 Aug 2017 14:27:39 -0700 Subject: [PATCH] modified the way custom services can be imported and added a test case to verify the import functionality --- daemon/core/service.py | 101 +++++++++++++++++++++------ daemon/examples/myservices/sample.py | 1 + daemon/tests/myservices/__init__.py | 0 daemon/tests/myservices/sample.py | 27 +++++++ daemon/tests/test_core.py | 12 ++++ 5 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 daemon/tests/myservices/__init__.py create mode 100644 daemon/tests/myservices/sample.py diff --git a/daemon/core/service.py b/daemon/core/service.py index 0649af73..5593df8d 100644 --- a/daemon/core/service.py +++ b/daemon/core/service.py @@ -6,21 +6,22 @@ The CoreServices class handles configuration messages for sending a list of available services to the GUI and for configuring individual services. """ - +import importlib +import inspect import os import shlex import sys import time from itertools import repeat -from core.api import coreapi from core.conf import Configurable from core.conf import ConfigurableManager -from core.data import EventData, ConfigData, FileData +from core.data import ConfigData +from core.data import EventData +from core.data import FileData from core.enumerations import ConfigDataTypes from core.enumerations import ConfigFlags from core.enumerations import EventTypes -from core.enumerations import FileTlvs from core.enumerations import MessageFlags from core.enumerations import RegisterTlvs from core.misc import log @@ -29,6 +30,78 @@ from core.misc import utils logger = log.get_logger(__name__) +def valid_module(path, file_name): + """ + Check if file is a valid python module. + + :param str path: path to file + :param str file_name: file name to check + :return: True if a valid python module file, False otherwise + :rtype: bool + """ + file_path = os.path.join(path, file_name) + if not os.path.isfile(file_path): + return False + + if file_name.startswith("_"): + return False + + if not file_name.endswith(".py"): + return False + + return True + + +def is_service(module, member): + """ + Validates if a module member is a class and an instance of a CoreService. + + :param module: module to validate for service + :param member: member to validate for service + :return: True if a valid service, False otherwise + :rtype: bool + """ + if not inspect.isclass(member): + return False + + if not issubclass(member, CoreService): + return False + + if member.__module__ != module.__name__: + return False + + return True + + +def get_services(path): + """ + Method for retrieving all CoreServices from a given path. + + :param str path: path to retrieve services from + :return: list of core services + :rtype: list + """ + logger.info("getting custom services from: %s", path) + parent_path = os.path.dirname(path) + logger.info("adding parent path to allow imports: %s", parent_path) + sys.path.append(parent_path) + base_module = os.path.basename(path) + module_names = os.listdir(path) + module_names = filter(lambda x: valid_module(path, x), module_names) + module_names = map(lambda x: x[:-3], module_names) + + custom_services = [] + for module_name in module_names: + import_statement = "%s.%s" % (base_module, module_name) + logger.info("importing custom service: %s", import_statement) + module = importlib.import_module(import_statement) + members = inspect.getmembers(module, lambda x: is_service(module, x)) + for member in members: + custom_services.append(member[1]) + + return custom_services + + class ServiceManager(object): """ Manages services available for CORE nodes to use. @@ -106,10 +179,6 @@ class CoreServices(ConfigurableManager): from core.services import startup self.is_startup_service = startup.Startup.is_startup_service - @classmethod - def add_service_path(cls, path): - cls.service_path.add(path) - def importcustom(self, path): """ Import services from a myservices directory. @@ -117,6 +186,7 @@ class CoreServices(ConfigurableManager): :param str path: path to import custom services from :return: nothing """ + if not path or len(path) == 0: return @@ -124,18 +194,9 @@ class CoreServices(ConfigurableManager): logger.warn("invalid custom service directory specified" ": %s" % path) return - try: - parentdir, childdir = os.path.split(path) - if childdir in self._invalid_custom_names: - raise ValueError("use a unique custom services dir name, " "not '%s'" % childdir) - if parentdir not in sys.path: - sys.path.append(parentdir) - # TODO: remove use of this exec statement - statement = "from %s import *" % childdir - logger.info("custom import: %s", statement) - exec (statement) - except: - logger.exception("error importing custom services from %s", path) + for service in get_services(path): + logger.info("adding new service to manager: %s", service) + ServiceManager.add(service) def reset(self): """ diff --git a/daemon/examples/myservices/sample.py b/daemon/examples/myservices/sample.py index 2433d81a..1beeebf7 100644 --- a/daemon/examples/myservices/sample.py +++ b/daemon/examples/myservices/sample.py @@ -64,6 +64,7 @@ class MyService(CoreService): return 'echo " network %s"' % net +# this is needed to load desired services when being integrated into core, otherwise this is not needed def load_services(): # this line is required to add the above class to the list of available services ServiceManager.add(MyService) diff --git a/daemon/tests/myservices/__init__.py b/daemon/tests/myservices/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/daemon/tests/myservices/sample.py b/daemon/tests/myservices/sample.py new file mode 100644 index 00000000..c986cc4a --- /dev/null +++ b/daemon/tests/myservices/sample.py @@ -0,0 +1,27 @@ +""" +Sample user-defined services for testing. +""" + +from core.service import CoreService + + +class MyService(CoreService): + _name = "MyService" + _group = "Utility" + _depends = () + _dirs = () + _configs = ('myservice.sh',) + _startindex = 50 + _startup = ('sh myservice.sh',) + _shutdown = () + + +class MyService2(CoreService): + _name = "MyService2" + _group = "Utility" + _depends = () + _dirs = () + _configs = ('myservice.sh',) + _startindex = 50 + _startup = ('sh myservice.sh',) + _shutdown = () diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 5aa927d1..2e9094cc 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -19,15 +19,27 @@ from core.mobility import BasicRangeModel from core.netns import nodes from core.netns import vnodeclient from core.phys.pnodes import PhysicalNode +from core.service import ServiceManager from core.xml import xmlsession _PATH = os.path.abspath(os.path.dirname(__file__)) +_SERVICES_PATH = os.path.join(_PATH, "myservices") _MOBILITY_FILE = os.path.join(_PATH, "mobility.scen") _XML_VERSIONS = ["0.0", "1.0"] _NODE_CLASSES = [nodes.PtpNet, nodes.HubNode, nodes.SwitchNode] class TestCore: + def test_import_service(self, core): + """ + Test importing a custom service. + + :param conftest.Core core: core fixture to test with + """ + core.session.services.importcustom(_SERVICES_PATH) + assert ServiceManager.get("MyService") + assert ServiceManager.get("MyService2") + @pytest.mark.parametrize("cls", _NODE_CLASSES) def test_nodes(self, core, cls): """