memmgr_test.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. # Copyright (C) 2013 Internet Systems Consortium.
  2. #
  3. # Permission to use, copy, modify, and distribute this software for any
  4. # purpose with or without fee is hereby granted, provided that the above
  5. # copyright notice and this permission notice appear in all copies.
  6. #
  7. # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
  8. # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
  9. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
  10. # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
  11. # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
  12. # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
  13. # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
  14. # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. import unittest
  16. import os
  17. import re
  18. import threading
  19. import isc.log
  20. from isc.dns import RRClass
  21. import isc.config
  22. from isc.config import parse_answer
  23. import memmgr
  24. from isc.memmgr.datasrc_info import SegmentInfo
  25. from isc.testutils.ccsession_mock import MockModuleCCSession
  26. class MyCCSession(MockModuleCCSession, isc.config.ConfigData):
  27. def __init__(self, specfile, config_handler, command_handler):
  28. super().__init__()
  29. specfile = os.environ['B10_FROM_BUILD'] + '/src/bin/memmgr/memmgr.spec'
  30. module_spec = isc.config.module_spec_from_file(specfile)
  31. isc.config.ConfigData.__init__(self, module_spec)
  32. self.add_remote_params = [] # for inspection
  33. self.add_remote_exception = None # to raise exception from the method
  34. def start(self):
  35. pass
  36. def add_remote_config_by_name(self, mod_name, handler):
  37. if self.add_remote_exception is not None:
  38. raise self.add_remote_exception
  39. self.add_remote_params.append((mod_name, handler))
  40. class MockMemmgr(memmgr.Memmgr):
  41. def _setup_ccsession(self):
  42. orig_cls = isc.config.ModuleCCSession
  43. isc.config.ModuleCCSession = MyCCSession
  44. try:
  45. super()._setup_ccsession()
  46. finally:
  47. isc.config.ModuleCCSession = orig_cls
  48. # Defined for easier tests with DataSrcClientsMgr.reconfigure(), which
  49. # only needs get_value() method
  50. class MockConfigData:
  51. def __init__(self, data):
  52. self.__data = data
  53. def get_value(self, identifier):
  54. return self.__data[identifier], False
  55. class TestMemmgr(unittest.TestCase):
  56. def setUp(self):
  57. # Some tests use this directory. Make sure it doesn't pre-exist.
  58. self.__test_mapped_file_dir = \
  59. os.environ['B10_FROM_BUILD'] + \
  60. '/src/bin/memmgr/tests/test_mapped_files'
  61. if os.path.isdir(self.__test_mapped_file_dir):
  62. os.rmdir(self.__test_mapped_file_dir)
  63. self.__mgr = MockMemmgr()
  64. # Fake some 'os' module functions for easier tests
  65. self.__orig_os_access = os.access
  66. self.__orig_isdir = os.path.isdir
  67. def tearDown(self):
  68. # Not all unittests cause this method to be called, so we call
  69. # it explicitly as it may be necessary in some cases where the
  70. # builder thread has been created.
  71. self.__mgr._shutdown_module()
  72. # Assert that all commands sent to the builder thread were
  73. # handled.
  74. self.assertEqual(len(self.__mgr._builder_command_queue), 0)
  75. # Restore faked values
  76. os.access = self.__orig_os_access
  77. os.path.isdir = self.__orig_isdir
  78. # If at test created a mapped-files directory, delete it.
  79. if os.path.isdir(self.__test_mapped_file_dir):
  80. os.rmdir(self.__test_mapped_file_dir)
  81. def test_init(self):
  82. """Check some initial conditions"""
  83. self.assertIsNone(self.__mgr._config_params)
  84. self.assertEqual([], self.__mgr._datasrc_info_list)
  85. # Try to configure a data source clients with the manager. This
  86. # should confirm the manager object is instantiated enabling in-memory
  87. # cache.
  88. cfg_data = MockConfigData(
  89. {"classes": {"IN": [{"type": "MasterFiles",
  90. "cache-enable": True, "params": {}}]}})
  91. self.__mgr._datasrc_clients_mgr.reconfigure({}, cfg_data)
  92. clist = \
  93. self.__mgr._datasrc_clients_mgr.get_client_list(RRClass.IN)
  94. self.assertEqual(1, len(clist.get_status()))
  95. def test_configure(self):
  96. self.__mgr._setup_ccsession()
  97. # Pretend specified directories exist and writable
  98. os.path.isdir = lambda x: True
  99. os.access = lambda x, y: True
  100. # At the initial configuration, if mapped_file_dir isn't specified,
  101. # the default value will be set.
  102. self.assertEqual((0, None),
  103. parse_answer(self.__mgr._config_handler({})))
  104. self.assertEqual('mapped_files',
  105. self.__mgr._config_params['mapped_file_dir'].
  106. split('/')[-1])
  107. # Update the configuration.
  108. user_cfg = {'mapped_file_dir': '/some/path/dir'}
  109. self.assertEqual((0, None),
  110. parse_answer(self.__mgr._config_handler(user_cfg)))
  111. self.assertEqual('/some/path/dir',
  112. self.__mgr._config_params['mapped_file_dir'])
  113. # Bad update: diretory doesn't exist (we assume it really doesn't
  114. # exist in the tested environment). Update won't be made.
  115. os.path.isdir = self.__orig_isdir # use real library
  116. user_cfg = {'mapped_file_dir': '/another/path/dir'}
  117. answer = parse_answer(self.__mgr._config_handler(user_cfg))
  118. self.assertEqual(1, answer[0])
  119. self.assertIsNotNone(re.search('not a directory', answer[1]))
  120. @unittest.skipIf(os.getuid() == 0,
  121. 'test cannot be run as root user')
  122. def test_configure_bad_permissions(self):
  123. self.__mgr._setup_ccsession()
  124. # Pretend specified directories exist and writable
  125. os.path.isdir = lambda x: True
  126. os.access = lambda x, y: True
  127. # Initial configuration.
  128. self.assertEqual((0, None),
  129. parse_answer(self.__mgr._config_handler({})))
  130. os.path.isdir = self.__orig_isdir
  131. os.access = self.__orig_os_access
  132. # Bad update: directory exists but is not writable.
  133. os.mkdir(self.__test_mapped_file_dir, 0o500) # drop writable bit
  134. user_cfg = {'mapped_file_dir': self.__test_mapped_file_dir}
  135. answer = parse_answer(self.__mgr._config_handler(user_cfg))
  136. self.assertEqual(1, answer[0])
  137. self.assertIsNotNone(re.search('not writable', answer[1]))
  138. def test_setup_module(self):
  139. # _setup_module should add data_sources remote module with
  140. # expected parameters.
  141. self.__mgr._setup_ccsession()
  142. self.assertEqual([], self.__mgr.mod_ccsession.add_remote_params)
  143. self.__mgr._setup_module()
  144. self.assertEqual([('data_sources',
  145. self.__mgr._datasrc_config_handler)],
  146. self.__mgr.mod_ccsession.add_remote_params)
  147. # If data source isn't configured it's considered fatal (checking the
  148. # same scenario with two possible exception types)
  149. self.__mgr.mod_ccsession.add_remote_exception = \
  150. isc.config.ModuleCCSessionError('faked exception')
  151. self.assertRaises(isc.server_common.bind10_server.BIND10ServerFatal,
  152. self.__mgr._setup_module)
  153. self.__mgr.mod_ccsession.add_remote_exception = \
  154. isc.config.ModuleSpecError('faked exception')
  155. self.assertRaises(isc.server_common.bind10_server.BIND10ServerFatal,
  156. self.__mgr._setup_module)
  157. def test_datasrc_config_handler(self):
  158. self.__mgr._config_params = {'mapped_file_dir': '/some/path'}
  159. # A simple (boring) case with real class implementations. This
  160. # confirms the methods are called as expected.
  161. cfg_data = MockConfigData(
  162. {"classes": {"IN": [{"type": "MasterFiles",
  163. "cache-enable": True, "params": {}}]}})
  164. self.__init_called = None
  165. def mock_init_segments(param):
  166. self.__init_called = param
  167. self.__mgr._init_segments = mock_init_segments
  168. self.__mgr._datasrc_config_handler({}, cfg_data)
  169. self.assertEqual(1, len(self.__mgr._datasrc_info_list))
  170. self.assertEqual(1, self.__mgr._datasrc_info_list[0].gen_id)
  171. self.assertEqual(self.__init_called, self.__mgr._datasrc_info_list[0])
  172. # Below we're using a mock DataSrcClientMgr for easier tests
  173. class MockDataSrcClientMgr:
  174. def __init__(self, status_list, raise_on_reconfig=False):
  175. self.__status_list = status_list
  176. self.__raise_on_reconfig = raise_on_reconfig
  177. def reconfigure(self, new_config, config_data):
  178. if self.__raise_on_reconfig:
  179. raise isc.server_common.datasrc_clients_mgr.ConfigError(
  180. 'test error')
  181. # otherwise do nothing
  182. def get_clients_map(self):
  183. return 42, {RRClass.IN: self}
  184. def get_status(self): # mocking get_clients_map()[1].get_status()
  185. return self.__status_list
  186. # This confirms memmgr's config is passed and handled correctly.
  187. # From memmgr's point of view it should be enough we have an object
  188. # in segment_info_map. Note also that the new DataSrcInfo is appended
  189. # to the list
  190. self.__mgr._datasrc_clients_mgr = \
  191. MockDataSrcClientMgr([('sqlite3', 'mapped', None)])
  192. self.__mgr._datasrc_config_handler(None, None) # params don't matter
  193. self.assertEqual(2, len(self.__mgr._datasrc_info_list))
  194. self.assertEqual(self.__init_called, self.__mgr._datasrc_info_list[1])
  195. self.assertIsNotNone(
  196. self.__mgr._datasrc_info_list[1].segment_info_map[
  197. (RRClass.IN, 'sqlite3')])
  198. # Emulate the case reconfigure() fails. Exception isn't propagated,
  199. # but the list doesn't change.
  200. self.__mgr._datasrc_clients_mgr = MockDataSrcClientMgr(None, True)
  201. self.__mgr._datasrc_config_handler(None, None)
  202. self.assertEqual(2, len(self.__mgr._datasrc_info_list))
  203. def test_init_segments(self):
  204. """
  205. Test the initialization of segments ‒ just load everything found in there.
  206. """
  207. # Fake a lot of things. These are objects hard to set up, so this is
  208. # easier.
  209. class SgmtInfo:
  210. def __init__(self):
  211. self.events = []
  212. self.__state = None
  213. def add_event(self, cmd):
  214. self.events.append(cmd)
  215. self.__state = SegmentInfo.UPDATING
  216. def start_update(self):
  217. return self.events[0]
  218. def get_state(self):
  219. return self.__state
  220. sgmt_info = SgmtInfo()
  221. class DataSrcInfo:
  222. def __init__(self):
  223. self.segment_info_map = \
  224. {(isc.dns.RRClass.IN, "name"): sgmt_info}
  225. dsrc_info = DataSrcInfo()
  226. # Pretend to have the builder thread
  227. self.__mgr._builder_cv = threading.Condition()
  228. # Run the initialization
  229. self.__mgr._init_segments(dsrc_info)
  230. # The event was pushed into the segment info
  231. command = ('load', None, dsrc_info, isc.dns.RRClass.IN, 'name')
  232. self.assertEqual([command], sgmt_info.events)
  233. self.assertEqual([command], self.__mgr._builder_command_queue)
  234. del self.__mgr._builder_command_queue[:]
  235. def test_notify_from_builder(self):
  236. """
  237. Check the notify from builder thing eats the notifications and
  238. handles them.
  239. """
  240. # Some mocks
  241. class SgmtInfo:
  242. pass
  243. sgmt_info = SgmtInfo
  244. class DataSrcInfo:
  245. def __init__(self):
  246. self.segment_info_map = \
  247. {(isc.dns.RRClass.IN, "name"): sgmt_info}
  248. dsrc_info = DataSrcInfo()
  249. class Sock:
  250. def recv(self, size):
  251. pass
  252. self.__mgr._master_sock = Sock()
  253. self.__mgr._builder_lock = threading.Lock()
  254. # Extract the reference for the queue. We get a copy of the reference
  255. # to check it is cleared, not a new empty one installed
  256. notif_ref = self.__mgr._builder_response_queue
  257. notif_ref.append(('load-completed', dsrc_info, isc.dns.RRClass.IN,
  258. 'name'))
  259. # Wake up the main thread and let it process the notifications
  260. self.__mgr._notify_from_builder()
  261. # All notifications are now eaten
  262. self.assertEqual([], notif_ref)
  263. if __name__== "__main__":
  264. isc.log.resetUnitTestRootLogger()
  265. unittest.main()