2
0
mirror of https://gitlab.isc.org/isc-projects/kea synced 2025-09-03 15:35:17 +00:00

[trac930] modify Stats

- remove unneeded subject and listener classes

 - add StatsError for handling errors in Stats

 - add some new methods (update_modules, update_statistics_data and
   get_statistics_data)

 - modify implementations of existent commands(show and set) according changes
   stats.spec

 - remove reset and remove command because stats module couldn't manage other
   modules' statistics data schema

 - add implementation of strict validation of each statistics data
   (If the validation is failed, it puts out the error.)

 - stats module shows its PID when status command invoked

 - add new command showschema invokable via bindctl

 - set command requires arguments of owner module name and statistics item name

 - show and showschema commands accepts arguments of owner module name and
   statistics item name

 - exits at exit code 1 if got runtime errors

 - has boot time in _BASETIME
This commit is contained in:
Naoki Kambe
2011-07-08 19:56:24 +09:00
parent daa1d6dd07
commit c074f6e0b7

View File

@@ -15,16 +15,17 @@
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""
Statistics daemon in BIND 10
"""
import sys; sys.path.append ('@@PYTHONPATH@@')
import os
import signal
import select
from time import time, strftime, gmtime
from optparse import OptionParser, OptionValueError
from collections import defaultdict
from isc.config.ccsession import ModuleCCSession, create_answer
from isc.cc import Session, SessionError
import isc
import isc.util.process
import isc.log
from stats_messages import *
@@ -35,352 +36,24 @@ logger = isc.log.Logger("stats")
# have #1074
DBG_STATS_MESSAGING = 30
# This is for boot_time of Stats
_BASETIME = gmtime()
# for setproctitle
import isc.util.process
isc.util.process.rename()
# If B10_FROM_SOURCE is set in the environment, we use data files
# from a directory relative to that, otherwise we use the ones
# installed on the system
if "B10_FROM_SOURCE" in os.environ:
BASE_LOCATION = os.environ["B10_FROM_SOURCE"] + os.sep + \
"src" + os.sep + "bin" + os.sep + "stats"
SPECFILE_LOCATION = os.environ["B10_FROM_SOURCE"] + os.sep + \
"src" + os.sep + "bin" + os.sep + "stats" + os.sep + "stats.spec"
else:
PREFIX = "@prefix@"
DATAROOTDIR = "@datarootdir@"
BASE_LOCATION = "@datadir@" + os.sep + "@PACKAGE@"
BASE_LOCATION = BASE_LOCATION.replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
SPECFILE_LOCATION = BASE_LOCATION + os.sep + "stats.spec"
SCHEMA_SPECFILE_LOCATION = BASE_LOCATION + os.sep + "stats-schema.spec"
class Singleton(type):
"""
A abstract class of singleton pattern
"""
# Because of singleton pattern:
# At the beginning of coding, one UNIX domain socket is needed
# for config manager, another socket is needed for stats module,
# then stats module might need two sockets. So I adopted the
# singleton pattern because I avoid creating multiple sockets in
# one stats module. But in the initial version stats module
# reports only via bindctl, so just one socket is needed. To use
# the singleton pattern is not important now. :(
def __init__(self, *args, **kwargs):
type.__init__(self, *args, **kwargs)
self._instances = {}
def __call__(self, *args, **kwargs):
if args not in self._instances:
self._instances[args]={}
kw = tuple(kwargs.items())
if kw not in self._instances[args]:
self._instances[args][kw] = type.__call__(self, *args, **kwargs)
return self._instances[args][kw]
class Callback():
"""
A Callback handler class
"""
def __init__(self, name=None, callback=None, args=(), kwargs={}):
self.name = name
self.callback = callback
self.args = args
self.kwargs = kwargs
def __call__(self, *args, **kwargs):
if not args:
args = self.args
if not kwargs:
kwargs = self.kwargs
if self.callback:
return self.callback(*args, **kwargs)
class Subject():
"""
A abstract subject class of observer pattern
"""
# Because of observer pattern:
# In the initial release, I'm also sure that observer pattern
# isn't definitely needed because the interface between gathering
# and reporting statistics data is single. However in the future
# release, the interfaces may be multiple, that is, multiple
# listeners may be needed. For example, one interface, which
# stats module has, is for between ''config manager'' and stats
# module, another interface is for between ''HTTP server'' and
# stats module, and one more interface is for between ''SNMP
# server'' and stats module. So by considering that stats module
# needs multiple interfaces in the future release, I adopted the
# observer pattern in stats module. But I don't have concrete
# ideas in case of multiple listener currently.
def __init__(self):
self._listeners = []
def attach(self, listener):
if not listener in self._listeners:
self._listeners.append(listener)
def detach(self, listener):
try:
self._listeners.remove(listener)
except ValueError:
pass
def notify(self, event, modifier=None):
for listener in self._listeners:
if modifier != listener:
listener.update(event)
class Listener():
"""
A abstract listener class of observer pattern
"""
def __init__(self, subject):
self.subject = subject
self.subject.attach(self)
self.events = {}
def update(self, name):
if name in self.events:
callback = self.events[name]
return callback()
def add_event(self, event):
self.events[event.name]=event
class SessionSubject(Subject, metaclass=Singleton):
"""
A concrete subject class which creates CC session object
"""
def __init__(self, session=None):
Subject.__init__(self)
self.session=session
self.running = False
def start(self):
self.running = True
self.notify('start')
def stop(self):
self.running = False
self.notify('stop')
def check(self):
self.notify('check')
class CCSessionListener(Listener):
"""
A concrete listener class which creates SessionSubject object and
ModuleCCSession object
"""
def __init__(self, subject):
Listener.__init__(self, subject)
self.session = subject.session
self.boot_time = get_datetime()
# create ModuleCCSession object
self.cc_session = ModuleCCSession(SPECFILE_LOCATION,
self.config_handler,
self.command_handler,
self.session)
self.session = self.subject.session = self.cc_session._session
# initialize internal data
self.stats_spec = isc.config.module_spec_from_file(SCHEMA_SPECFILE_LOCATION).get_config_spec()
self.stats_data = self.initialize_data(self.stats_spec)
# add event handler invoked via SessionSubject object
self.add_event(Callback('start', self.start))
self.add_event(Callback('stop', self.stop))
self.add_event(Callback('check', self.check))
# don't add 'command_' suffix to the special commands in
# order to prevent executing internal command via bindctl
# get commands spec
self.commands_spec = self.cc_session.get_module_spec().get_commands_spec()
# add event handler related command_handler of ModuleCCSession
# invoked via bindctl
for cmd in self.commands_spec:
try:
# add prefix "command_"
name = "command_" + cmd["command_name"]
callback = getattr(self, name)
kwargs = self.initialize_data(cmd["command_args"])
self.add_event(Callback(name=name, callback=callback, args=(), kwargs=kwargs))
except AttributeError as ae:
logger.error(STATS_UNKNOWN_COMMAND_IN_SPEC, cmd["command_name"])
def start(self):
"""
start the cc chanel
"""
# set initial value
self.stats_data['stats.boot_time'] = self.boot_time
self.stats_data['stats.start_time'] = get_datetime()
self.stats_data['stats.last_update_time'] = get_datetime()
self.stats_data['stats.lname'] = self.session.lname
self.cc_session.start()
# request Bob to send statistics data
logger.debug(DBG_STATS_MESSAGING, STATS_SEND_REQUEST_BOSS)
cmd = isc.config.ccsession.create_command("sendstats", None)
seq = self.session.group_sendmsg(cmd, 'Boss')
self.session.group_recvmsg(True, seq)
def stop(self):
"""
stop the cc chanel
"""
return self.cc_session.close()
def check(self):
"""
check the cc chanel
"""
return self.cc_session.check_command(False)
def config_handler(self, new_config):
"""
handle a configure from the cc channel
"""
logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_NEW_CONFIG,
new_config)
# do nothing currently
return create_answer(0)
def command_handler(self, command, *args, **kwargs):
"""
handle commands from the cc channel
"""
# add 'command_' suffix in order to executing command via bindctl
name = 'command_' + command
if name in self.events:
event = self.events[name]
return event(*args, **kwargs)
else:
return self.command_unknown(command, args)
def command_shutdown(self, args):
"""
handle shutdown command
"""
logger.info(STATS_RECEIVED_SHUTDOWN_COMMAND)
self.subject.running = False
return create_answer(0)
def command_set(self, args, stats_data={}):
"""
handle set command
"""
# 'args' must be dictionary type
self.stats_data.update(args['stats_data'])
# overwrite "stats.LastUpdateTime"
self.stats_data['stats.last_update_time'] = get_datetime()
return create_answer(0)
def command_remove(self, args, stats_item_name=''):
"""
handle remove command
"""
# 'args' must be dictionary type
if args and args['stats_item_name'] in self.stats_data:
stats_item_name = args['stats_item_name']
logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_REMOVE_COMMAND,
stats_item_name)
# just remove one item
self.stats_data.pop(stats_item_name)
return create_answer(0)
def command_show(self, args, stats_item_name=''):
"""
handle show command
"""
# always overwrite 'report_time' and 'stats.timestamp'
# if "show" command invoked
self.stats_data['report_time'] = get_datetime()
self.stats_data['stats.timestamp'] = get_timestamp()
# if with args
if args and args['stats_item_name'] in self.stats_data:
stats_item_name = args['stats_item_name']
logger.debug(DBG_STATS_MESSAGING,
STATS_RECEIVED_SHOW_NAME_COMMAND,
stats_item_name)
return create_answer(0, {stats_item_name: self.stats_data[stats_item_name]})
logger.debug(DBG_STATS_MESSAGING,
STATS_RECEIVED_SHOW_ALL_COMMAND)
return create_answer(0, self.stats_data)
def command_reset(self, args):
"""
handle reset command
"""
logger.debug(DBG_STATS_MESSAGING,
STATS_RECEIVED_RESET_COMMAND)
# re-initialize internal variables
self.stats_data = self.initialize_data(self.stats_spec)
# reset initial value
self.stats_data['stats.boot_time'] = self.boot_time
self.stats_data['stats.start_time'] = get_datetime()
self.stats_data['stats.last_update_time'] = get_datetime()
self.stats_data['stats.lname'] = self.session.lname
return create_answer(0)
def command_status(self, args):
"""
handle status command
"""
logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_STATUS_COMMAND)
# just return "I'm alive."
return create_answer(0, "I'm alive.")
def command_unknown(self, command, args):
"""
handle an unknown command
"""
logger.error(STATS_RECEIVED_UNKNOWN_COMMAND, command)
return create_answer(1, "Unknown command: '"+str(command)+"'")
def initialize_data(self, spec):
"""
initialize stats data
"""
def __get_init_val(spec):
if spec['item_type'] == 'null':
return None
elif spec['item_type'] == 'boolean':
return bool(spec.get('item_default', False))
elif spec['item_type'] == 'string':
return str(spec.get('item_default', ''))
elif spec['item_type'] in set(['number', 'integer']):
return int(spec.get('item_default', 0))
elif spec['item_type'] in set(['float', 'double', 'real']):
return float(spec.get('item_default', 0.0))
elif spec['item_type'] in set(['list', 'array']):
return spec.get('item_default',
[ __get_init_val(s) for s in spec['list_item_spec'] ])
elif spec['item_type'] in set(['map', 'object']):
return spec.get('item_default',
dict([ (s['item_name'], __get_init_val(s)) for s in spec['map_item_spec'] ]) )
else:
return spec.get('item_default')
return dict([ (s['item_name'], __get_init_val(s)) for s in spec ])
SPECFILE_LOCATION = "@datadir@" + os.sep + "@PACKAGE@" + os.sep + "stats.spec"
SPECFILE_LOCATION = SPECFILE_LOCATION.replace("${datarootdir}", DATAROOTDIR)\
.replace("${prefix}", PREFIX)
def get_timestamp():
"""
@@ -388,33 +61,314 @@ def get_timestamp():
"""
return time()
def get_datetime():
def get_datetime(gmt=None):
"""
get current datetime
"""
return strftime("%Y-%m-%dT%H:%M:%SZ", gmtime())
if not gmt: gmt = gmtime()
return strftime("%Y-%m-%dT%H:%M:%SZ", gmt)
def main(session=None):
def parse_spec(spec):
"""
parse spec type data
"""
def _parse_spec(spec):
item_type = spec['item_type']
if item_type == "integer":
return int(spec.get('item_default', 0))
elif item_type == "real":
return float(spec.get('item_default', 0.0))
elif item_type == "boolean":
return bool(spec.get('item_default', False))
elif item_type == "string":
return str(spec.get('item_default', ""))
elif item_type == "list":
return spec.get(
"item_default",
[ _parse_spec(s) for s in spec["list_item_spec"] ])
elif item_type == "map":
return spec.get(
"item_default",
dict([ (s["item_name"], _parse_spec(s)) for s in spec["map_item_spec"] ]) )
else:
return spec.get("item_default", None)
return dict([ (s['item_name'], _parse_spec(s)) for s in spec ])
class Callback():
"""
A Callback handler class
"""
def __init__(self, command=None, args=(), kwargs={}):
self.command = command
self.args = args
self.kwargs = kwargs
def __call__(self, *args, **kwargs):
if not args: args = self.args
if not kwargs: kwargs = self.kwargs
if self.command: return self.command(*args, **kwargs)
class StatsError(Exception):
"""Exception class for Stats class"""
pass
class Stats:
"""
Main class of stats module
"""
def __init__(self):
self.running = False
# create ModuleCCSession object
self.mccs = isc.config.ModuleCCSession(SPECFILE_LOCATION,
self.config_handler,
self.command_handler)
self.cc_session = self.mccs._session
# get module spec
self.module_name = self.mccs.get_module_spec().get_module_name()
self.modules = {}
self.statistics_data = {}
# get commands spec
self.commands_spec = self.mccs.get_module_spec().get_commands_spec()
# add event handler related command_handler of ModuleCCSession
self.callbacks = {}
for cmd in self.commands_spec:
# add prefix "command_"
name = "command_" + cmd["command_name"]
try:
callback = getattr(self, name)
kwargs = parse_spec(cmd["command_args"])
self.callbacks[name] = Callback(command=callback, kwargs=kwargs)
except AttributeError:
raise StatsError(STATS_UNKNOWN_COMMAND_IN_SPEC, cmd["command_name"])
self.mccs.start()
def start(self):
"""
Start stats module
"""
self.running = True
# TODO: should be added into new logging interface
# if self.verbose:
# sys.stdout.write("[b10-stats] starting\n")
# request Bob to send statistics data
logger.debug(DBG_STATS_MESSAGING, STATS_SEND_REQUEST_BOSS)
cmd = isc.config.ccsession.create_command("sendstats", None)
seq = self.cc_session.group_sendmsg(cmd, 'Boss')
self.cc_session.group_recvmsg(True, seq)
# initialized Statistics data
errors = self.update_statistics_data(
self.module_name,
lname=self.cc_session.lname,
boot_time=get_datetime(_BASETIME)
)
if errors:
raise StatsError("stats spec file is incorrect")
while self.running:
self.mccs.check_command(False)
def config_handler(self, new_config):
"""
handle a configure from the cc channel
"""
logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_NEW_CONFIG,
new_config)
# do nothing currently
return isc.config.create_answer(0)
def command_handler(self, command, kwargs):
"""
handle commands from the cc channel
"""
name = 'command_' + command
if name in self.callbacks:
callback = self.callbacks[name]
if kwargs:
return callback(**kwargs)
else:
return callback()
else:
logger.error(STATS_RECEIVED_UNKNOWN_COMMAND, command)
return isc.config.create_answer(1, "Unknown command: '"+str(command)+"'")
def update_modules(self):
"""
update information of each module
"""
modules = {}
seq = self.cc_session.group_sendmsg(
isc.config.ccsession.create_command(
isc.config.ccsession.COMMAND_GET_STATISTICS_SPEC),
'ConfigManager')
(answer, env) = self.cc_session.group_recvmsg(False, seq)
if answer:
(rcode, value) = isc.config.ccsession.parse_answer(answer)
if rcode == 0:
for mod in value:
spec = { "module_name" : mod,
"statistics" : [] }
if value[mod] and type(value[mod]) is list:
spec["statistics"] = value[mod]
modules[mod] = isc.config.module_spec.ModuleSpec(spec)
modules[self.module_name] = self.mccs.get_module_spec()
self.modules = modules
def get_statistics_data(self, owner=None, name=None):
"""
return statistics data which stats module has of each module
"""
self.update_statistics_data()
if owner and name:
try:
return self.statistics_data[owner][name]
except KeyError:
pass
elif owner:
try:
return self.statistics_data[owner]
except KeyError:
pass
elif name:
pass
else:
return self.statistics_data
def update_statistics_data(self, owner=None, **data):
"""
change statistics date of specified module into specified data
"""
self.update_modules()
statistics_data = {}
for (name, module) in self.modules.items():
value = parse_spec(module.get_statistics_spec())
if module.validate_statistics(True, value):
statistics_data[name] = value
for (name, value) in self.statistics_data.items():
if name in statistics_data:
statistics_data[name].update(value)
else:
statistics_data[name] = value
self.statistics_data = statistics_data
if owner and data:
errors = []
try:
if self.modules[owner].validate_statistics(False, data, errors):
self.statistics_data[owner].update(data)
return
except KeyError:
errors.append('unknown module name')
return errors
def command_status(self):
"""
handle status command
"""
logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_STATUS_COMMAND)
return isc.config.create_answer(
0, "Stats is up. (PID " + str(os.getpid()) + ")")
def command_shutdown(self):
"""
handle shutdown command
"""
logger.info(STATS_RECEIVED_SHUTDOWN_COMMAND)
self.running = False
return isc.config.create_answer(0)
def command_show(self, owner=None, name=None):
"""
handle show command
"""
if (owner or name):
logger.debug(DBG_STATS_MESSAGING,
STATS_RECEIVED_SHOW_NAME_COMMAND,
str(owner)+", "+str(name))
else:
logger.debug(DBG_STATS_MESSAGING,
STATS_RECEIVED_SHOW_ALL_COMMAND)
if owner and not name:
return isc.config.create_answer(1, "item name is not specified")
errors = self.update_statistics_data(
self.module_name,
timestamp=get_timestamp(),
report_time=get_datetime()
)
if errors: raise StatsError("stats spec file is incorrect")
ret = self.get_statistics_data(owner, name)
if ret:
return isc.config.create_answer(0, ret)
else:
return isc.config.create_answer(
1, "specified module name and/or item name are incorrect")
def command_showschema(self, owner=None, name=None):
"""
handle show command
"""
# TODO: should be added into new logging interface
# if self.verbose:
# sys.stdout.write("[b10-stats] 'showschema' command received\n")
self.update_modules()
schema = {}
schema_byname = {}
for mod in self.modules:
spec = self.modules[mod].get_statistics_spec()
schema_byname[mod] = {}
if spec:
schema[mod] = spec
for item in spec:
schema_byname[mod][item['item_name']] = item
if owner:
try:
if name:
return isc.config.create_answer(0, schema_byname[owner][name])
else:
return isc.config.create_answer(0, schema[owner])
except KeyError:
pass
else:
if name:
return isc.config.create_answer(1, "module name is not specified")
else:
return isc.config.create_answer(0, schema)
return isc.config.create_answer(
1, "specified module name and/or item name are incorrect")
def command_set(self, owner, data):
"""
handle set command
"""
errors = self.update_statistics_data(owner, **data)
if errors:
return isc.config.create_answer(
1,
"specified module name and/or statistics data are incorrect: "
+ ", ".join(errors))
errors = self.update_statistics_data(
self.module_name, last_update_time=get_datetime() )
if errors:
raise StatsError("stats spec file is incorrect")
return isc.config.create_answer(0)
if __name__ == "__main__":
try:
parser = OptionParser()
parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
help="display more about what is going on")
parser.add_option(
"-v", "--verbose", dest="verbose", action="store_true",
help="display more about what is going on")
(options, args) = parser.parse_args()
if options.verbose:
isc.log.init("b10-stats", "DEBUG", 99)
subject = SessionSubject(session=session)
listener = CCSessionListener(subject)
subject.start()
while subject.running:
subject.check()
subject.stop()
stats = Stats()
stats.start()
except OptionValueError as ove:
logger.fatal(STATS_BAD_OPTION_VALUE, ove)
except SessionError as se:
logger.fatal(STATS_CC_SESSION_ERROR, se)
# TODO: should be added into new logging interface
except StatsError as se:
sys.exit("[b10-stats] %s" % se)
except KeyboardInterrupt as kie:
logger.info(STATS_STOPPED_BY_KEYBOARD)
if __name__ == "__main__":
main()