1087 lines
39 KiB
Python
1087 lines
39 KiB
Python
"""
|
|
Definition of CoreService class that is subclassed to define
|
|
startup services and routing for nodes. A service is typically a daemon
|
|
program launched when a node starts that provides some sort of service.
|
|
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 import logger
|
|
from core.conf import Configurable
|
|
from core.conf import ConfigurableManager
|
|
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 MessageFlags
|
|
from core.enumerations import RegisterTlvs
|
|
from core.misc import utils
|
|
|
|
|
|
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
|
|
|
|
|
|
class ServiceManager(object):
|
|
"""
|
|
Manages services available for CORE nodes to use.
|
|
"""
|
|
services = []
|
|
|
|
@classmethod
|
|
def add(cls, service):
|
|
"""
|
|
Add a service to manager.
|
|
|
|
:param CoreService service: service to add
|
|
:return: nothing
|
|
"""
|
|
insert = 0
|
|
for index, known_service in enumerate(cls.services):
|
|
if known_service._group == service._group:
|
|
insert = index + 1
|
|
break
|
|
|
|
logger.info("loading service: %s - %s: %s", insert, service, service._name)
|
|
cls.services.insert(insert, service)
|
|
|
|
@classmethod
|
|
def get(cls, name):
|
|
"""
|
|
Retrieve a service from the manager.
|
|
|
|
:param str name: name of the service to retrieve
|
|
:return: service if it exists, None otherwise
|
|
:rtype: CoreService
|
|
"""
|
|
for service in cls.services:
|
|
if service._name == name:
|
|
return service
|
|
return None
|
|
|
|
@classmethod
|
|
def add_services(cls, 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
|
|
"""
|
|
# validate path exists for importing
|
|
logger.info("getting custom services from: %s", path)
|
|
parent_path = os.path.dirname(path)
|
|
if parent_path not in sys.path:
|
|
logger.info("adding parent path to allow imports: %s", parent_path)
|
|
sys.path.append(parent_path)
|
|
|
|
# retrieve potential service modules, and filter out invalid modules
|
|
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)
|
|
|
|
# import and add all service modules in the path
|
|
for module_name in module_names:
|
|
import_statement = "%s.%s" % (base_module, module_name)
|
|
logger.info("importing custom service module: %s", import_statement)
|
|
try:
|
|
module = importlib.import_module(import_statement)
|
|
members = inspect.getmembers(module, lambda x: _is_service(module, x))
|
|
for member in members:
|
|
clazz = member[1]
|
|
clazz.on_load()
|
|
cls.add(clazz)
|
|
except:
|
|
logger.exception("unexpected error during import, skipping: %s", import_statement)
|
|
|
|
|
|
class CoreServices(ConfigurableManager):
|
|
"""
|
|
Class for interacting with a list of available startup services for
|
|
nodes. Mostly used to convert a CoreService into a Config API
|
|
message. This class lives in the Session object and remembers
|
|
the default services configured for each node type, and any
|
|
custom service configuration. A CoreService is not a Configurable.
|
|
"""
|
|
name = "services"
|
|
config_type = RegisterTlvs.UTILITY.value
|
|
|
|
_invalid_custom_names = (
|
|
'core', 'addons', 'api', 'bsd', 'emane', 'misc', 'netns', 'phys', 'services', 'xen'
|
|
)
|
|
|
|
def __init__(self, session):
|
|
"""
|
|
Creates a CoreServices instance.
|
|
|
|
:param core.session.Session session: session this manager is tied to
|
|
:return: nothing
|
|
"""
|
|
ConfigurableManager.__init__(self)
|
|
self.session = session
|
|
# dict of default services tuples, key is node type
|
|
self.defaultservices = {}
|
|
# dict of tuple of service objects, key is node number
|
|
self.customservices = {}
|
|
|
|
paths = self.session.get_config_item('custom_services_dir')
|
|
if paths:
|
|
for path in paths.split(','):
|
|
path = path.strip()
|
|
self.importcustom(path)
|
|
|
|
# TODO: remove need for cyclic import
|
|
from core.services import startup
|
|
self.is_startup_service = startup.Startup.is_startup_service
|
|
|
|
def importcustom(self, path):
|
|
"""
|
|
Import services from a myservices directory.
|
|
|
|
:param str path: path to import custom services from
|
|
:return: nothing
|
|
"""
|
|
|
|
if not path or len(path) == 0:
|
|
return
|
|
|
|
if not os.path.isdir(path):
|
|
logger.warn("invalid custom service directory specified" ": %s" % path)
|
|
return
|
|
|
|
ServiceManager.add_services(path)
|
|
|
|
def reset(self):
|
|
"""
|
|
Called when config message with reset flag is received
|
|
"""
|
|
self.defaultservices.clear()
|
|
self.customservices.clear()
|
|
|
|
def getdefaultservices(self, service_type):
|
|
"""
|
|
Get the list of default services that should be enabled for a
|
|
node for the given node type.
|
|
|
|
:param service_type: service type to get default services for
|
|
:return: default services
|
|
:rtype: list
|
|
"""
|
|
logger.debug("getting default services for type: %s", service_type)
|
|
results = []
|
|
if service_type in self.defaultservices:
|
|
defaults = self.defaultservices[service_type]
|
|
for name in defaults:
|
|
logger.debug("checking for service with service manager: %s", name)
|
|
service = ServiceManager.get(name)
|
|
if not service:
|
|
logger.warn("default service %s is unknown", name)
|
|
else:
|
|
results.append(service)
|
|
return results
|
|
|
|
def getcustomservice(self, object_id, service):
|
|
"""
|
|
Get any custom service configured for the given node that matches the specified service name.
|
|
If no custom service is found, return the specified service.
|
|
|
|
:param int object_id: object id to get service from
|
|
:param CoreService service: custom service to retrieve
|
|
:return: custom service from the node
|
|
:rtype: CoreService
|
|
"""
|
|
if object_id in self.customservices:
|
|
for s in self.customservices[object_id]:
|
|
if s._name == service._name:
|
|
return s
|
|
return service
|
|
|
|
def setcustomservice(self, object_id, service, values):
|
|
"""
|
|
Store service customizations in an instantiated service object
|
|
using a list of values that came from a config message.
|
|
|
|
:param int object_id: object id to set custom service for
|
|
:param class service: service to set
|
|
:param list values: values to
|
|
:return:
|
|
"""
|
|
if service._custom:
|
|
s = service
|
|
else:
|
|
# instantiate the class, for storing config customization
|
|
s = service()
|
|
# values are new key=value format; not all keys need to be present
|
|
# a missing key means go with the default
|
|
if Configurable.haskeyvalues(values):
|
|
for v in values:
|
|
key, value = v.split('=', 1)
|
|
s.setvalue(key, value)
|
|
# old-style config, list of values
|
|
else:
|
|
s.fromvaluelist(values)
|
|
|
|
# assume custom service already in dict
|
|
if service._custom:
|
|
return
|
|
# add the custom service to dict
|
|
if object_id in self.customservices:
|
|
self.customservices[object_id] += (s,)
|
|
else:
|
|
self.customservices[object_id] = (s,)
|
|
|
|
def addservicestonode(self, node, nodetype, services_str):
|
|
"""
|
|
Populate the node.service list using (1) the list of services
|
|
requested from the services TLV, (2) using any custom service
|
|
configuration, or (3) using the default services for this node type.
|
|
|
|
:param core.coreobj.PyCoreNode node: node to add services to
|
|
:param str nodetype: node type to add services to
|
|
:param str services_str: string formatted service list
|
|
:return: nothing
|
|
"""
|
|
if services_str is not None:
|
|
logger.info("setting node specific services: %s", services_str)
|
|
services = services_str.split("|")
|
|
for name in services:
|
|
s = ServiceManager.get(name)
|
|
if s is None:
|
|
logger.warn("configured service %s for node %s is unknown", name, node.name)
|
|
continue
|
|
logger.info("adding configured service %s to node %s", s._name, node.name)
|
|
s = self.getcustomservice(node.objid, s)
|
|
node.addservice(s)
|
|
else:
|
|
logger.info("setting default services for node (%s) type (%s)", node.objid, nodetype)
|
|
services = self.getdefaultservices(nodetype)
|
|
for s in services:
|
|
logger.info("adding default service %s to node %s", s._name, node.name)
|
|
s = self.getcustomservice(node.objid, s)
|
|
node.addservice(s)
|
|
|
|
def getallconfigs(self, use_clsmap=True):
|
|
"""
|
|
Return (nodenum, service) tuples for all stored configs. Used when reconnecting to a
|
|
session or opening XML.
|
|
|
|
:param bool use_clsmap: should a class map be used, default to True
|
|
:return: list of tuples of node ids and services
|
|
:rtype: list
|
|
"""
|
|
configs = []
|
|
for nodenum in self.customservices:
|
|
for service in self.customservices[nodenum]:
|
|
configs.append((nodenum, service))
|
|
return configs
|
|
|
|
def getallfiles(self, service):
|
|
"""
|
|
Return all customized files stored with a service.
|
|
Used when reconnecting to a session or opening XML.
|
|
|
|
:param CoreService service: service to get files for
|
|
:return:
|
|
"""
|
|
files = []
|
|
|
|
if not service._custom:
|
|
return files
|
|
|
|
for filename in service._configs:
|
|
data = self.getservicefiledata(service, filename)
|
|
if data is None:
|
|
continue
|
|
files.append((filename, data))
|
|
|
|
return files
|
|
|
|
def bootnodeservices(self, node):
|
|
"""
|
|
Start all services on a node.
|
|
|
|
:param core.netns.nodes.CoreNode node: node to start services on
|
|
:return:
|
|
"""
|
|
services = sorted(node.services, key=lambda service: service._startindex)
|
|
use_startup_service = any(map(self.is_startup_service, services))
|
|
for s in services:
|
|
if len(str(s._starttime)) > 0:
|
|
try:
|
|
t = float(s._starttime)
|
|
if t > 0.0:
|
|
fn = self.bootnodeservice
|
|
self.session.event_loop.add_event(t, fn, node, s, services, False)
|
|
continue
|
|
except ValueError:
|
|
logger.exception("error converting start time to float")
|
|
self.bootnodeservice(node, s, services, use_startup_service)
|
|
|
|
def bootnodeservice(self, node, service, services, use_startup_service):
|
|
"""
|
|
Start a service on a node. Create private dirs, generate config
|
|
files, and execute startup commands.
|
|
|
|
:param core.netns.nodes.CoreNode node: node to boot services on
|
|
:param CoreService service: service to start
|
|
:param list services: service list
|
|
:param bool use_startup_service: flag to use startup services or not
|
|
:return: nothing
|
|
"""
|
|
if service._custom:
|
|
self.bootnodecustomservice(node, service, services, use_startup_service)
|
|
return
|
|
|
|
logger.info("starting service %s (%s)" % (service._name, service._startindex))
|
|
for directory in service._dirs:
|
|
try:
|
|
node.privatedir(directory)
|
|
except:
|
|
logger.exception("Error making node %s dir %s", node.name, directory)
|
|
|
|
for filename in service.getconfigfilenames(node.objid, services):
|
|
cfg = service.generateconfig(node, filename, services)
|
|
node.nodefile(filename, cfg)
|
|
|
|
if use_startup_service and not self.is_startup_service(service):
|
|
return
|
|
|
|
for cmd in service.getstartup(node, services):
|
|
try:
|
|
# NOTE: this wait=False can be problematic!
|
|
node.cmd(shlex.split(cmd), wait=False)
|
|
except:
|
|
logger.exception("error starting command %s", cmd)
|
|
|
|
def bootnodecustomservice(self, node, service, services, use_startup_service):
|
|
"""
|
|
Start a custom service on a node. Create private dirs, use supplied
|
|
config files, and execute supplied startup commands.
|
|
|
|
:param core.netns.nodes.CoreNode node: node to boot services on
|
|
:param CoreService service: service to start
|
|
:param list services: service list
|
|
:param bool use_startup_service: flag to use startup services or not
|
|
:return: nothing
|
|
"""
|
|
logger.info("starting service(%s) %s (%s)(custom)",
|
|
service, service._name, service._startindex)
|
|
for directory in service._dirs:
|
|
try:
|
|
node.privatedir(directory)
|
|
except:
|
|
logger.exception("error making node %s dir %s", node.name, directory)
|
|
|
|
logger.info("service configurations: %s", service._configs)
|
|
for i, filename in enumerate(service._configs):
|
|
logger.info("generating service config: %s", filename)
|
|
if len(filename) == 0:
|
|
continue
|
|
cfg = self.getservicefiledata(service, filename)
|
|
if cfg is None:
|
|
cfg = service.generateconfig(node, filename, services)
|
|
# cfg may have a file:/// url for copying from a file
|
|
try:
|
|
if self.copyservicefile(node, filename, cfg):
|
|
continue
|
|
except IOError:
|
|
logger.exception("error copying service file '%s'", filename)
|
|
continue
|
|
node.nodefile(filename, cfg)
|
|
|
|
if use_startup_service and not self.is_startup_service(service):
|
|
return
|
|
|
|
for cmd in service._startup:
|
|
try:
|
|
# NOTE: this wait=False can be problematic!
|
|
node.cmd(shlex.split(cmd), wait=False)
|
|
except:
|
|
logger.exception("error starting command %s", cmd)
|
|
|
|
def copyservicefile(self, node, filename, cfg):
|
|
"""
|
|
Given a configured service filename and config, determine if the
|
|
config references an existing file that should be copied.
|
|
Returns True for local files, False for generated.
|
|
|
|
:param core.netns.nodes.CoreNode node: node to copy service for
|
|
:param str filename: file name for a configured service
|
|
:param str cfg: configuration string
|
|
:return: True if successful, False otherwise
|
|
:rtype: bool
|
|
"""
|
|
if cfg[:7] == 'file://':
|
|
src = cfg[7:]
|
|
src = src.split('\n')[0]
|
|
src = utils.expandcorepath(src, node.session, node)
|
|
# TODO: glob here
|
|
node.nodefilecopy(filename, src, mode=0644)
|
|
return True
|
|
return False
|
|
|
|
def validatenodeservices(self, node):
|
|
"""
|
|
Run validation commands for all services on a node.
|
|
|
|
:param core.netns.nodes.CoreNode node: node to validate services for
|
|
:return: nothing
|
|
"""
|
|
services = sorted(node.services, key=lambda service: service._startindex)
|
|
for s in services:
|
|
self.validatenodeservice(node, s, services)
|
|
|
|
def validatenodeservice(self, node, service, services):
|
|
"""
|
|
Run the validation command(s) for a service.
|
|
|
|
:param core.netns.nodes.CoreNode node: node to validate service for
|
|
:param CoreService service: service to validate
|
|
:param list services: services for node
|
|
:return: service validation status
|
|
:rtype: int
|
|
"""
|
|
logger.info("validating service for node (%s - %s): %s (%s)",
|
|
node.objid, node.name, service._name, service._startindex)
|
|
if service._custom:
|
|
validate_cmds = service._validate
|
|
else:
|
|
validate_cmds = service.getvalidate(node, services)
|
|
|
|
status = 0
|
|
# has validate commands
|
|
if len(validate_cmds) > 0:
|
|
for cmd in validate_cmds:
|
|
logger.info("validating service %s using: %s", service._name, cmd)
|
|
try:
|
|
status, result = node.cmdresult(shlex.split(cmd))
|
|
if status != 0:
|
|
raise ValueError("non-zero exit status")
|
|
except:
|
|
logger.exception("validate command failed: %s", cmd)
|
|
status = -1
|
|
|
|
return status
|
|
|
|
def stopnodeservices(self, node):
|
|
"""
|
|
Stop all services on a node.
|
|
|
|
:param core.netns.nodes.CoreNode node: node to stop services on
|
|
:return: nothing
|
|
"""
|
|
services = sorted(node.services, key=lambda service: service._startindex)
|
|
for s in services:
|
|
self.stopnodeservice(node, s)
|
|
|
|
def stopnodeservice(self, node, service):
|
|
"""
|
|
Stop a service on a node.
|
|
|
|
:param core.netns.nodes.CoreNode node: node to stop a service on
|
|
:param CoreService service: service to stop
|
|
:return: status for stopping the services
|
|
:rtype: str
|
|
"""
|
|
status = ""
|
|
if len(service._shutdown) == 0:
|
|
# doesn't have a shutdown command
|
|
status += "0"
|
|
else:
|
|
for cmd in service._shutdown:
|
|
try:
|
|
tmp = node.cmd(shlex.split(cmd), wait=True)
|
|
status += "%s" % tmp
|
|
except:
|
|
logger.exception("error running stop command %s", cmd)
|
|
status += "-1"
|
|
return status
|
|
|
|
def configure_request(self, config_data):
|
|
"""
|
|
Receive configuration message for configuring services.
|
|
With a request flag set, a list of services has been requested.
|
|
When the opaque field is present, a specific service is being
|
|
configured or requested.
|
|
|
|
:param core.conf.ConfigData config_data: configuration data for carrying out a configuration
|
|
:return: response messages
|
|
:rtype: ConfigData
|
|
"""
|
|
node_id = config_data.node
|
|
session_id = config_data.session
|
|
opaque = config_data.opaque
|
|
|
|
logger.info("configuration request: node(%s) session(%s) opaque(%s)", node_id, session_id, opaque)
|
|
|
|
# send back a list of available services
|
|
if opaque is None:
|
|
type_flag = ConfigFlags.NONE.value
|
|
data_types = tuple(repeat(ConfigDataTypes.BOOL.value, len(ServiceManager.services)))
|
|
values = "|".join(repeat('0', len(ServiceManager.services)))
|
|
names = map(lambda x: x._name, ServiceManager.services)
|
|
captions = "|".join(names)
|
|
possible_values = ""
|
|
for s in ServiceManager.services:
|
|
if s._custom_needed:
|
|
possible_values += '1'
|
|
possible_values += '|'
|
|
groups = self.buildgroups(ServiceManager.services)
|
|
# send back the properties for this service
|
|
else:
|
|
if node_id is None:
|
|
return None
|
|
node = self.session.get_object(node_id)
|
|
if node is None:
|
|
logger.warn("Request to configure service for unknown node %s", node_id)
|
|
return None
|
|
servicesstring = opaque.split(':')
|
|
services, unknown = self.servicesfromopaque(opaque, node.objid)
|
|
for u in unknown:
|
|
logger.warn("Request for unknown service '%s'" % u)
|
|
|
|
if len(services) < 1:
|
|
return None
|
|
|
|
if len(servicesstring) == 3:
|
|
# a file request: e.g. "service:zebra:quagga.conf"
|
|
file_data = self.getservicefile(services, node, servicesstring[2])
|
|
self.session.broadcast_file(file_data)
|
|
|
|
# short circuit this request early to avoid returning response below
|
|
return None
|
|
|
|
# the first service in the list is the one being configured
|
|
svc = services[0]
|
|
# send back:
|
|
# dirs, configs, startindex, startup, shutdown, metadata, config
|
|
type_flag = ConfigFlags.UPDATE.value
|
|
data_types = tuple(repeat(ConfigDataTypes.STRING.value, len(svc.keys)))
|
|
values = svc.tovaluelist(node, services)
|
|
captions = None
|
|
possible_values = None
|
|
groups = None
|
|
|
|
return ConfigData(
|
|
message_type=0,
|
|
node=node_id,
|
|
object=self.name,
|
|
type=type_flag,
|
|
data_types=data_types,
|
|
data_values=values,
|
|
captions=captions,
|
|
possible_values=possible_values,
|
|
groups=groups,
|
|
session=session_id,
|
|
opaque=opaque
|
|
)
|
|
|
|
def configure_values(self, config_data):
|
|
"""
|
|
Receive configuration message for configuring services.
|
|
With a request flag set, a list of services has been requested.
|
|
When the opaque field is present, a specific service is being
|
|
configured or requested.
|
|
|
|
:param core.conf.ConfigData config_data: configuration data for carrying out a configuration
|
|
:return: None
|
|
"""
|
|
data_types = config_data.data_types
|
|
values = config_data.data_values
|
|
node_id = config_data.node
|
|
opaque = config_data.opaque
|
|
|
|
error_message = "services config message that I don't know how to handle"
|
|
if values is None:
|
|
logger.error(error_message)
|
|
return None
|
|
else:
|
|
values = values.split('|')
|
|
|
|
if opaque is None:
|
|
# store default services for a node type in self.defaultservices[]
|
|
if data_types is None or data_types[0] != ConfigDataTypes.STRING.value:
|
|
logger.info(error_message)
|
|
return None
|
|
key = values.pop(0)
|
|
self.defaultservices[key] = values
|
|
logger.info("default services for type %s set to %s" % (key, values))
|
|
else:
|
|
# store service customized config in self.customservices[]
|
|
if node_id is None:
|
|
return None
|
|
services, unknown = self.servicesfromopaque(opaque, node_id)
|
|
for u in unknown:
|
|
logger.warn("Request for unknown service '%s'" % u)
|
|
|
|
if len(services) < 1:
|
|
return None
|
|
svc = services[0]
|
|
self.setcustomservice(node_id, svc, values)
|
|
|
|
return None
|
|
|
|
def servicesfromopaque(self, opaque, object_id):
|
|
"""
|
|
Build a list of services from an opaque data string.
|
|
|
|
:param str opaque: opaque data string
|
|
:param int object_id: object id
|
|
:return: services and unknown services lists tuple
|
|
:rtype: tuple
|
|
"""
|
|
services = []
|
|
unknown = []
|
|
servicesstring = opaque.split(':')
|
|
if servicesstring[0] != "service":
|
|
return []
|
|
servicenames = servicesstring[1].split(',')
|
|
for name in servicenames:
|
|
s = ServiceManager.get(name)
|
|
s = self.getcustomservice(object_id, s)
|
|
if s is None:
|
|
unknown.append(name)
|
|
else:
|
|
services.append(s)
|
|
return services, unknown
|
|
|
|
def buildgroups(self, servicelist):
|
|
"""
|
|
Build a string of groups for use in a configuration message given
|
|
a list of services. The group list string has the format
|
|
"title1:1-5|title2:6-9|10-12", where title is an optional group title
|
|
and i-j is a numeric range of value indices; groups are
|
|
separated by commas.
|
|
|
|
:param list servicelist: service list to build group string from
|
|
:return: groups string
|
|
:rtype: str
|
|
"""
|
|
i = 0
|
|
r = ""
|
|
lastgroup = "<undefined>"
|
|
for service in servicelist:
|
|
i += 1
|
|
group = service._group
|
|
if group != lastgroup:
|
|
lastgroup = group
|
|
# finish previous group
|
|
if i > 1:
|
|
r += "-%d|" % (i - 1)
|
|
# optionally include group title
|
|
if group == "":
|
|
r += "%d" % i
|
|
else:
|
|
r += "%s:%d" % (group, i)
|
|
# finish the last group list
|
|
if i > 0:
|
|
r += "-%d" % i
|
|
return r
|
|
|
|
def getservicefile(self, services, node, filename):
|
|
"""
|
|
Send a File Message when the GUI has requested a service file.
|
|
The file data is either auto-generated or comes from an existing config.
|
|
|
|
:param list services: service list
|
|
:param core.netns.nodes.CoreNode node: node to get service file from
|
|
:param str filename: file name to retrieve
|
|
:return: file message for node
|
|
"""
|
|
svc = services[0]
|
|
# get the filename and determine the config file index
|
|
if svc._custom:
|
|
cfgfiles = svc._configs
|
|
else:
|
|
cfgfiles = svc.getconfigfilenames(node.objid, services)
|
|
if filename not in cfgfiles:
|
|
logger.warn("Request for unknown file '%s' for service '%s'" % (filename, services[0]))
|
|
return None
|
|
|
|
# get the file data
|
|
data = self.getservicefiledata(svc, filename)
|
|
if data is None:
|
|
data = "%s" % (svc.generateconfig(node, filename, services))
|
|
else:
|
|
data = "%s" % data
|
|
filetypestr = "service:%s" % svc._name
|
|
|
|
return FileData(
|
|
message_type=MessageFlags.ADD.value,
|
|
node=node.objid,
|
|
name=filename,
|
|
type=filetypestr,
|
|
data=data
|
|
)
|
|
|
|
def getservicefiledata(self, service, filename):
|
|
"""
|
|
Get the customized file data associated with a service. Return None
|
|
for invalid filenames or missing file data.
|
|
|
|
:param CoreService service: service to get file data from
|
|
:param str filename: file name to get data from
|
|
:return: file data
|
|
"""
|
|
try:
|
|
i = service._configs.index(filename)
|
|
except ValueError:
|
|
return None
|
|
if i >= len(service._configtxt) or service._configtxt[i] is None:
|
|
return None
|
|
return service._configtxt[i]
|
|
|
|
def setservicefile(self, nodenum, type, filename, srcname, data):
|
|
"""
|
|
Receive a File Message from the GUI and store the customized file
|
|
in the service config. The filename must match one from the list of
|
|
config files in the service.
|
|
|
|
:param int nodenum: node id to set service file
|
|
:param str type: file type to set
|
|
:param str filename: file name to set
|
|
:param str srcname: source name of file to set
|
|
:param data: data for file to set
|
|
:return: nothing
|
|
"""
|
|
if len(type.split(':')) < 2:
|
|
logger.warn("Received file type did not contain service info.")
|
|
return
|
|
if srcname is not None:
|
|
raise NotImplementedError
|
|
svcid, svcname = type.split(':')[:2]
|
|
svc = ServiceManager.get(svcname)
|
|
svc = self.getcustomservice(nodenum, svc)
|
|
if svc is None:
|
|
logger.warn("Received filename for unknown service '%s'" % svcname)
|
|
return
|
|
cfgfiles = svc._configs
|
|
if filename not in cfgfiles:
|
|
logger.warn("Received unknown file '%s' for service '%s'" % (filename, svcname))
|
|
return
|
|
i = cfgfiles.index(filename)
|
|
configtxtlist = list(svc._configtxt)
|
|
numitems = len(configtxtlist)
|
|
if numitems < i + 1:
|
|
# add empty elements to list to support index assignment
|
|
for j in range(1, (i + 2) - numitems):
|
|
configtxtlist += None,
|
|
configtxtlist[i] = data
|
|
svc._configtxt = configtxtlist
|
|
|
|
def handleevent(self, event_data):
|
|
"""
|
|
Handle an Event Message used to start, stop, restart, or validate
|
|
a service on a given node.
|
|
|
|
:param EventData event_data: event data to handle
|
|
:return: nothing
|
|
"""
|
|
event_type = event_data.event_type
|
|
node_id = event_data.node
|
|
name = event_data.name
|
|
|
|
try:
|
|
node = self.session.get_object(node_id)
|
|
except KeyError:
|
|
logger.warn("Ignoring event for service '%s', unknown node '%s'", name, node_id)
|
|
return
|
|
|
|
fail = ""
|
|
services, unknown = self.servicesfromopaque(name, node_id)
|
|
for s in services:
|
|
if event_type == EventTypes.STOP.value or event_type == EventTypes.RESTART.value:
|
|
status = self.stopnodeservice(node, s)
|
|
if status != "0":
|
|
fail += "Stop %s," % s._name
|
|
if event_type == EventTypes.START.value or event_type == EventTypes.RESTART.value:
|
|
if s._custom:
|
|
cmds = s._startup
|
|
else:
|
|
cmds = s.getstartup(node, services)
|
|
if len(cmds) > 0:
|
|
for cmd in cmds:
|
|
try:
|
|
# node.cmd(shlex.split(cmd), wait = False)
|
|
status = node.cmd(shlex.split(cmd), wait=True)
|
|
if status != 0:
|
|
fail += "Start %s(%s)," % (s._name, cmd)
|
|
except:
|
|
logger.exception("error starting command %s", cmd)
|
|
fail += "Start %s," % s._name
|
|
if event_type == EventTypes.PAUSE.value:
|
|
status = self.validatenodeservice(node, s, services)
|
|
if status != 0:
|
|
fail += "%s," % s._name
|
|
if event_type == EventTypes.RECONFIGURE.value:
|
|
if s._custom:
|
|
cfgfiles = s._configs
|
|
else:
|
|
cfgfiles = s.getconfigfilenames(node.objid, services)
|
|
if len(cfgfiles) > 0:
|
|
for filename in cfgfiles:
|
|
if filename[:7] == "file:///":
|
|
# TODO: implement this
|
|
raise NotImplementedError
|
|
cfg = self.getservicefiledata(s, filename)
|
|
if cfg is None:
|
|
cfg = s.generateconfig(node, filename, services)
|
|
try:
|
|
node.nodefile(filename, cfg)
|
|
except:
|
|
logger.exception("error in configure file: %s", filename)
|
|
fail += "%s," % s._name
|
|
|
|
fail_data = ""
|
|
if len(fail) > 0:
|
|
fail_data += "Fail:" + fail
|
|
unknown_data = ""
|
|
num = len(unknown)
|
|
if num > 0:
|
|
for u in unknown:
|
|
unknown_data += u
|
|
if num > 1:
|
|
unknown_data += ", "
|
|
num -= 1
|
|
logger.warn("Event requested for unknown service(s): %s", unknown_data)
|
|
unknown_data = "Unknown:" + unknown_data
|
|
|
|
event_data = EventData(
|
|
node=node_id,
|
|
event_type=event_type,
|
|
name=name,
|
|
data=fail_data + ";" + unknown_data,
|
|
time="%s" % time.time()
|
|
)
|
|
|
|
self.session.broadcast_event(event_data)
|
|
|
|
|
|
class CoreService(object):
|
|
"""
|
|
Parent class used for defining services.
|
|
"""
|
|
# service name should not include spaces
|
|
_name = ""
|
|
# group string allows grouping services together
|
|
_group = ""
|
|
# list name(s) of services that this service depends upon
|
|
_depends = ()
|
|
keys = ["dirs", "files", "startidx", "cmdup", "cmddown", "cmdval", "meta", "starttime"]
|
|
# private, per-node directories required by this service
|
|
_dirs = ()
|
|
# config files written by this service
|
|
_configs = ()
|
|
# index used to determine start order with other services
|
|
_startindex = 0
|
|
# time in seconds after runtime to run startup commands
|
|
_starttime = ""
|
|
# list of startup commands
|
|
_startup = ()
|
|
# list of shutdown commands
|
|
_shutdown = ()
|
|
# list of validate commands
|
|
_validate = ()
|
|
# metadata associated with this service
|
|
_meta = ""
|
|
# custom configuration text
|
|
_configtxt = ()
|
|
_custom = False
|
|
_custom_needed = False
|
|
|
|
def __init__(self):
|
|
"""
|
|
Services are not necessarily instantiated. Classmethods may be used
|
|
against their config. Services are instantiated when a custom
|
|
configuration is used to override their default parameters.
|
|
"""
|
|
self._custom = True
|
|
|
|
@classmethod
|
|
def on_load(cls):
|
|
pass
|
|
|
|
@classmethod
|
|
def getconfigfilenames(cls, nodenum, services):
|
|
"""
|
|
Return the tuple of configuration file filenames. This default method
|
|
returns the cls._configs tuple, but this method may be overriden to
|
|
provide node-specific filenames that may be based on other services.
|
|
|
|
:param int nodenum: node id to get config file names for
|
|
:param list services: node services
|
|
:return: class configuration files
|
|
:rtype: tuple
|
|
"""
|
|
return cls._configs
|
|
|
|
@classmethod
|
|
def generateconfig(cls, node, filename, services):
|
|
"""
|
|
Generate configuration file given a node object. The filename is
|
|
provided to allow for multiple config files. The other services are
|
|
provided to allow interdependencies (e.g. zebra and OSPF).
|
|
Return the configuration string to be written to a file or sent
|
|
to the GUI for customization.
|
|
|
|
:param core.netns.nodes.CoreNode node: node to generate config for
|
|
:param str filename: file name to generate config for
|
|
:param list services: services for node
|
|
:return: nothing
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@classmethod
|
|
def getstartup(cls, node, services):
|
|
"""
|
|
Return the tuple of startup commands. This default method
|
|
returns the cls._startup tuple, but this method may be
|
|
overridden to provide node-specific commands that may be
|
|
based on other services.
|
|
|
|
:param core.netns.nodes.CoreNode node: node to get startup for
|
|
:param list services: services for node
|
|
:return: startup commands
|
|
:rtype: tuple
|
|
"""
|
|
return cls._startup
|
|
|
|
@classmethod
|
|
def getvalidate(cls, node, services):
|
|
"""
|
|
Return the tuple of validate commands. This default method
|
|
returns the cls._validate tuple, but this method may be
|
|
overriden to provide node-specific commands that may be
|
|
based on other services.
|
|
|
|
:param core.netns.nodes.CoreNode node: node to validate
|
|
:param list services: services for node
|
|
:return: validation commands
|
|
:rtype: tuple
|
|
"""
|
|
return cls._validate
|
|
|
|
@classmethod
|
|
def tovaluelist(cls, node, services):
|
|
"""
|
|
Convert service properties into a string list of key=value pairs,
|
|
separated by "|".
|
|
|
|
:param core.netns.nodes.CoreNode node: node to get value list for
|
|
:param list services: services for node
|
|
:return: value list string
|
|
:rtype: str
|
|
"""
|
|
valmap = [cls._dirs, cls._configs, cls._startindex, cls._startup,
|
|
cls._shutdown, cls._validate, cls._meta, cls._starttime]
|
|
if not cls._custom:
|
|
# this is always reached due to classmethod
|
|
valmap[valmap.index(cls._configs)] = \
|
|
cls.getconfigfilenames(node.objid, services)
|
|
valmap[valmap.index(cls._startup)] = \
|
|
cls.getstartup(node, services)
|
|
vals = map(lambda a, b: "%s=%s" % (a, str(b)), cls.keys, valmap)
|
|
return "|".join(vals)
|
|
|
|
def fromvaluelist(self, values):
|
|
"""
|
|
Convert list of values into properties for this instantiated
|
|
(customized) service.
|
|
|
|
:param list values: value list to set properties from
|
|
:return: nothing
|
|
"""
|
|
# TODO: support empty value? e.g. override default meta with ''
|
|
for key in self.keys:
|
|
try:
|
|
self.setvalue(key, values[self.keys.index(key)])
|
|
except IndexError:
|
|
# old config does not need to have new keys
|
|
logger.exception("error indexing into key")
|
|
|
|
def setvalue(self, key, value):
|
|
"""
|
|
Set values for this service.
|
|
|
|
:param str key: key to set value for
|
|
:param value: value of key to set
|
|
:return: nothing
|
|
"""
|
|
if key not in self.keys:
|
|
raise ValueError('key `%s` not in `%s`' % (key, self.keys))
|
|
# this handles data conversion to int, string, and tuples
|
|
if value:
|
|
if key == "startidx":
|
|
value = int(value)
|
|
elif key == "meta":
|
|
value = str(value)
|
|
else:
|
|
value = utils.maketuplefromstr(value, str)
|
|
|
|
if key == "dirs":
|
|
self._dirs = value
|
|
elif key == "files":
|
|
self._configs = value
|
|
elif key == "startidx":
|
|
self._startindex = value
|
|
elif key == "cmdup":
|
|
self._startup = value
|
|
elif key == "cmddown":
|
|
self._shutdown = value
|
|
elif key == "cmdval":
|
|
self._validate = value
|
|
elif key == "meta":
|
|
self._meta = value
|
|
elif key == "starttime":
|
|
self._starttime = value
|