Commit e606d8d0 authored by JINMEI Tatuya's avatar JINMEI Tatuya
Browse files

[master] Merge branch 'trac2689'

parents 4c9255af d6e34fc0
......@@ -190,12 +190,19 @@ class Stats:
"""
Main class of stats module
"""
def __init__(self):
def __init__(self, module_ccsession_class=isc.config.ModuleCCSession):
'''Constructor
module_ccsession_class is parameterized so that test can specify
a mocked class to test the behavior without involing network I/O.
In other cases this parameter shouldn't be specified.
'''
self.running = False
# create ModuleCCSession object
self.mccs = isc.config.ModuleCCSession(SPECFILE_LOCATION,
self.config_handler,
self.command_handler)
self.mccs = module_ccsession_class(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()
......@@ -225,7 +232,16 @@ class Stats:
])
# set a absolute timestamp polling at next time
self.next_polltime = get_timestamp() + self.get_interval()
# initialized Statistics data
self._init_statistics_data()
def _init_statistics_data(self):
"""initialized Statistics data.
This method is a dedicated subroutine of __int__(), but extracted
so tests can override it to avoid blocking network operation.
"""
self.update_modules()
if self.update_statistics_data(
self.module_name,
......@@ -316,11 +332,9 @@ class Stats:
while len(sequences) > 0:
try:
(module_name, seq) = sequences.pop(0)
answer, env = self.cc_session.group_recvmsg(
False, seq)
answer, env = self.cc_session.group_recvmsg(False, seq)
if answer:
rcode, args = isc.config.ccsession.parse_answer(
answer)
rcode, args = isc.config.ccsession.parse_answer(answer)
if rcode == 0:
_statistics_data.append(
(module_name, env['from'], args))
......@@ -347,31 +361,35 @@ class Stats:
# if successfully done, set the last time of polling
self._lasttime_poll = get_timestamp()
def _check_command(self, nonblock=False):
"""check invoked command by waiting for 'poll-interval' seconds
This is a dedicated subroutine of start(), but extracted and defined
as a 'protected' method so that tests can replace it.
"""
# backup original timeout
orig_timeout = self.cc_session.get_timeout()
# set cc-session timeout to half of a second(500ms)
self.cc_session.set_timeout(500)
try:
answer, env = self.cc_session.group_recvmsg(nonblock)
self.mccs.check_command_without_recvmsg(answer, env)
except isc.cc.session.SessionTimeout:
pass # waited for poll-interval seconds
# restore timeout
self.cc_session.set_timeout(orig_timeout)
def start(self):
"""
Start stats module
"""
logger.info(STATS_STARTING)
def _check_command(nonblock=False):
"""check invoked command by waiting for 'poll-interval'
seconds"""
# backup original timeout
orig_timeout = self.cc_session.get_timeout()
# set cc-session timeout to half of a second(500ms)
self.cc_session.set_timeout(500)
try:
answer, env = self.cc_session.group_recvmsg(nonblock)
self.mccs.check_command_without_recvmsg(answer, env)
except isc.cc.session.SessionTimeout:
pass # waited for poll-interval seconds
# restore timeout
self.cc_session.set_timeout(orig_timeout)
try:
self.running = True
while self.running:
_check_command()
self._check_command()
now = get_timestamp()
intval = self.get_interval()
if intval > 0 and now >= self.next_polltime:
......@@ -476,16 +494,16 @@ class Stats:
updates statistics data. If specified data is invalid for
statistics spec of specified owner, it returns a list of error
messages. If there is no error or if neither owner nor data is
specified in args, it returns None. The 'mid' argument is an identifier of
the sender module in order for stats to identify which
specified in args, it returns None. The 'mid' argument is an
identifier of the sender module in order for stats to identify which
instance sends statistics data in the situation that multiple
instances are working.
"""
# Note:
# The fix of #1751 is for multiple instances working. It is
# assumed here that they send different statistics data with
# each sender module id (mid). Stats should save their statistics data by
# mid. The statistics data, which is the existing variable, is
# each sender module id (mid). Stats should save their statistics data
# by mid. The statistics data, which is the existing variable, is
# preserved by accumlating from statistics data by the mid. This
# is an ad-hoc fix because administrators can not see
# statistics by each instance via bindctl or HTTP/XML. These
......@@ -535,8 +553,7 @@ class Stats:
# merge recursively old value and new
# value each other
_data[owner][mid] = \
merge_oldnew(_data[owner][mid],
{_key: _val})
merge_oldnew(_data[owner][mid], {_key: _val})
continue
# the key string might be a "xx/yy/zz[0]"
# type. try it.
......@@ -546,22 +563,20 @@ class Stats:
if errors: errors.pop()
# try updata and check validation in adavance
__data = _data.copy()
if owner not in _data:
if owner not in __data:
__data[owner] = {}
if mid not in _data[owner]:
if mid not in __data[owner]:
__data[owner][mid] = {}
# use the isc.cc.data.set method
try:
isc.cc.data.set(__data[owner][mid],
_key, _val)
isc.cc.data.set(__data[owner][mid], _key, _val)
if self.modules[owner].validate_statistics(
False, __data[owner][mid], errors):
_data = __data
except Exception as e:
errors.append(
"%s: %s" % (e.__class__.__name__, e))
errors.append("%s: %s" % (e.__class__.__name__, e))
except KeyError:
errors.append("unknown module name: " + str(owner))
errors.append("unknown module name: " + owner)
if not errors:
self.statistics_data_bymid = _data
......@@ -584,8 +599,7 @@ class Stats:
# values are not replaced.
self.statistics_data[m] = merge_oldnew(
self.statistics_data[m],
_accum_bymodule(
self.statistics_data_bymid[m]))
_accum_bymodule(self.statistics_data_bymid[m]))
if errors: return errors
......
......@@ -48,7 +48,7 @@ import stats_httpd
import stats
from test_utils import BaseModules, ThreadingServerManager, MyStats,\
MyStatsHttpd, SignalHandler,\
send_command, send_shutdown, CONST_BASETIME
send_command, CONST_BASETIME
from isc.testutils.ccsession_mock import MockModuleCCSession
# This test suite uses xml.etree.ElementTree.XMLParser via
......@@ -461,7 +461,8 @@ class TestHttpHandler(unittest.TestCase):
(0, "Stats is up. (PID " + str(os.getpid()) + ")"))
# failure case(Stats is down)
self.assertTrue(self.stats.running)
self.assertEqual(send_shutdown("Stats"), (0, None)) # Stats is down
self.assertEqual(send_command("shutdown", "Stats"),
(0, None)) # Stats is down
self.assertFalse(self.stats.running)
self.stats_httpd.cc_session.set_timeout(milliseconds=100)
......@@ -608,8 +609,16 @@ class TestStatsHttpd(unittest.TestCase):
self.stats_server.run()
# checking IPv6 enabled on this platform
self.ipv6_enabled = is_ipv6_enabled()
# instantiation of StatsHttpd indirectly calls gethostbyaddr(), which
# can block for an uncontrollable period, leading many undesirable
# results. We should rather eliminate the reliance, but until we
# can make such fundamental cleanup we replace it with a faked method;
# in our test scenario the return value doesn't matter.
self.__gethostbyaddr_orig = socket.gethostbyaddr
socket.gethostbyaddr = lambda x: ('test.example.', [], None)
def tearDown(self):
socket.gethostbyaddr = self.__gethostbyaddr_orig
if hasattr(self, "stats_httpd"):
self.stats_httpd.stop()
self.stats_server.shutdown()
......@@ -751,7 +760,7 @@ class TestStatsHttpd(unittest.TestCase):
self.stats_httpd_server = ThreadingServerManager(MyStatsHttpd, server_addresses)
self.stats_httpd_server.run()
self.assertRaises(stats_httpd.HttpServerError, MyStatsHttpd, server_addresses)
send_shutdown("StatsHttpd")
send_command("shutdown", "StatsHttpd")
def test_running(self):
self.stats_httpd_server = ThreadingServerManager(MyStatsHttpd, get_availaddr())
......@@ -761,7 +770,7 @@ class TestStatsHttpd(unittest.TestCase):
self.assertEqual(send_command("status", "StatsHttpd"),
(0, "Stats Httpd is up. (PID " + str(os.getpid()) + ")"))
self.assertTrue(self.stats_httpd.running)
self.assertEqual(send_shutdown("StatsHttpd"), (0, None))
self.assertEqual(send_command("shutdown", "StatsHttpd"), (0, None))
self.assertFalse(self.stats_httpd.running)
self.stats_httpd_server.shutdown()
......
This diff is collapsed.
......@@ -51,30 +51,18 @@ class SignalHandler():
"""envokes unittest.TestCase.fail as a signal handler"""
self.fail_handler("A deadlock might be detected")
def send_command(command_name, module_name, params=None, session=None, nonblock=False, timeout=None):
if session is not None:
cc_session = session
else:
cc_session = isc.cc.Session()
if timeout is not None:
orig_timeout = cc_session.get_timeout()
cc_session.set_timeout(timeout * 1000)
def send_command(command_name, module_name, params=None):
cc_session = isc.cc.Session()
command = isc.config.ccsession.create_command(command_name, params)
seq = cc_session.group_sendmsg(command, module_name)
try:
(answer, env) = cc_session.group_recvmsg(nonblock, seq)
(answer, env) = cc_session.group_recvmsg(False, seq)
if answer:
return isc.config.ccsession.parse_answer(answer)
except isc.cc.SessionTimeout:
pass
finally:
if timeout is not None:
cc_session.set_timeout(orig_timeout)
if session is None:
cc_session.close()
def send_shutdown(module_name, **kwargs):
return send_command("shutdown", module_name, **kwargs)
cc_session.close()
class ThreadingServerManager:
def __init__(self, server, *args, **kwargs):
......@@ -467,6 +455,140 @@ class MockAuth:
return isc.config.create_answer(0, sdata)
return isc.config.create_answer(1, "Unknown Command")
class MyModuleCCSession(isc.config.ConfigData):
"""Mocked ModuleCCSession class.
This class incorporates the module spec directly from the file,
and works as if the ModuleCCSession class as much as possible
without involving network I/O.
"""
def __init__(self, spec_file, config_handler, command_handler):
module_spec = isc.config.module_spec_from_file(spec_file)
isc.config.ConfigData.__init__(self, module_spec)
self._session = self
self.stopped = False
self.lname = 'mock_mod_ccs'
def start(self):
pass
def send_stopping(self):
self.stopped = True # just record it's called to inspect it later
class SimpleStats(stats.Stats):
"""A faked Stats class for unit tests.
This class inherits most of the real Stats class, but replace the
ModuleCCSession with a fake one so we can avoid network I/O in tests,
and can also inspect or tweak messages via the session more easily.
This class also maintains some faked module information and statistics
data that can be retrieved from the implementation of the Stats class.
"""
def __init__(self):
# First, setup some internal attributes. All of them are essentially
# private (so prefixed with double '_'), but some are defined as if
# "protected" (with a single '_') for the convenient of tests that
# may want to inspect or tweak them.
# initial seq num for faked group_sendmsg, arbitrary choice.
self.__seq = 4200
# if set, use them as faked response to group_recvmsg (see below).
# it's a list of tuples, each of which is of (answer, envelope).
self._answers = []
# the default answer from faked recvmsg if _answers is empty
self.__default_answer = isc.config.ccsession.create_answer(
0, {'Init':
json.loads(MockInit.spec_str)['module_spec']['statistics'],
'Auth':
json.loads(MockAuth.spec_str)['module_spec']['statistics']
})
# setup faked auth statistics
self.__init_auth_stat()
# statistics data for faked Init module
self._init_sdata = {
'boot_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME)
}
# Incorporate other setups of the real Stats module. We use the faked
# ModuleCCSession to avoid blocking network operation. Note also that
# we replace _init_statistics_data() (see below), so we don't
# initialize statistics data yet.
stats.Stats.__init__(self, MyModuleCCSession)
# replace some (faked) ModuleCCSession methods so we can inspect/fake
# the data exchanged via the CC session, then call
# _init_statistics_data. This will get the Stats module info from
# the file directly and some amount information about the Init and
# Auth modules (hardcoded below).
self.cc_session.group_sendmsg = self.__group_sendmsg
self.cc_session.group_recvmsg = self.__group_recvmsg
self.cc_session.rpc_call = self.__rpc_call
stats.Stats._init_statistics_data(self)
def __init_auth_stat(self):
self._queries_tcp = 3
self._queries_udp = 2
self.__queries_per_zone = [{
'zonename': 'test1.example', 'queries.tcp': 5, 'queries.udp': 4
}]
self.__nds_queries_per_zone = \
{ 'test10.example': { 'queries.tcp': 5, 'queries.udp': 4 } }
self._auth_sdata = \
{ 'queries.tcp': self._queries_tcp,
'queries.udp': self._queries_udp,
'queries.perzone' : self.__queries_per_zone,
'nds_queries.perzone' : {
'test10.example': {
'queries.tcp': isc.cc.data.find(
self.__nds_queries_per_zone,
'test10.example/queries.tcp')
}
},
'nds_queries.perzone/test10.example/queries.udp' :
isc.cc.data.find(self.__nds_queries_per_zone,
'test10.example/queries.udp')
}
def _init_statistics_data(self):
# Inherited from real Stats class, just for deferring the
# initialization until we are ready.
pass
def __group_sendmsg(self, command, destination, want_answer=False):
"""Faked ModuleCCSession.group_sendmsg for tests.
Skipping actual network communication, and just returning an internally
generated sequence number.
"""
self.__seq += 1
return self.__seq
def __group_recvmsg(self, nonblocking, seq):
"""Faked ModuleCCSession.group_recvmsg for tests.
Skipping actual network communication, and returning an internally
prepared answer. sequence number. If faked anser is given in
_answers, use it; otherwise use the default. we don't actually check
the sequence.
"""
if len(self._answers) == 0:
return self.__default_answer, {'from': 'no-matter'}
return self._answers.pop(0)
def __rpc_call(self, command, group):
"""Faked ModuleCCSession.rpc_call for tests.
At the moment we don't have to cover failure cases, so this is a
simple wrapper for the faked group_recvmsg().
"""
answer, _ = self.__group_recvmsg(None, None)
return isc.config.ccsession.parse_answer(answer)[1]
class MyStats(stats.Stats):
stats._BASETIME = CONST_BASETIME
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment