zonemgr.py.in 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. #!@PYTHON@
  2. # Copyright (C) 2010 Internet Systems Consortium.
  3. #
  4. # Permission to use, copy, modify, and distribute this software for any
  5. # purpose with or without fee is hereby granted, provided that the above
  6. # copyright notice and this permission notice appear in all copies.
  7. #
  8. # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
  9. # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
  10. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
  11. # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
  12. # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
  13. # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
  14. # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
  15. # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  16. """\
  17. This file implements the Secondary Manager program.
  18. The secondary manager is one of the co-operating processes
  19. of BIND10, which keeps track of timers and other information
  20. necessary for BIND10 to act as a slave.
  21. """
  22. import sys; sys.path.append ('@@PYTHONPATH@@')
  23. import os
  24. import time
  25. import signal
  26. import isc
  27. import random
  28. import threading
  29. import select
  30. import socket
  31. import errno
  32. from isc.datasrc import sqlite3_ds
  33. from optparse import OptionParser, OptionValueError
  34. from isc.config.ccsession import *
  35. # If B10_FROM_BUILD is set in the environment, we use data files
  36. # from a directory relative to that, otherwise we use the ones
  37. # installed on the system
  38. if "B10_FROM_BUILD" in os.environ:
  39. SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/zonemgr"
  40. AUTH_SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/auth"
  41. else:
  42. PREFIX = "@prefix@"
  43. DATAROOTDIR = "@datarootdir@"
  44. SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
  45. AUTH_SPECFILE_PATH = SPECFILE_PATH
  46. SPECFILE_LOCATION = SPECFILE_PATH + "/zonemgr.spec"
  47. AUTH_SPECFILE_LOCATION = AUTH_SPECFILE_PATH + "/auth.spec"
  48. __version__ = "BIND10"
  49. # define module name
  50. XFRIN_MODULE_NAME = 'Xfrin'
  51. AUTH_MODULE_NAME = 'Auth'
  52. # define command name
  53. ZONE_XFRIN_FAILED_COMMAND = 'zone_xfrin_failed'
  54. ZONE_XFRIN_SUCCESS_COMMAND = 'zone_new_data_ready'
  55. ZONE_REFRESH_COMMAND = 'refresh_from_zonemgr'
  56. ZONE_NOTIFY_COMMAND = 'notify'
  57. # define zone state
  58. ZONE_OK = 0
  59. ZONE_REFRESHING = 1
  60. ZONE_EXPIRED = 2
  61. # smallest refresh timeout
  62. LOWERBOUND_REFRESH = 10
  63. # smallest retry timeout
  64. LOWERBOUND_RETRY = 5
  65. # max zone transfer timeout
  66. MAX_TRANSFER_TIMEOUT = 14400
  67. # offsets of fields in the SOA RDATA
  68. REFRESH_OFFSET = 3
  69. RETRY_OFFSET = 4
  70. EXPIRED_OFFSET = 5
  71. # verbose mode
  72. VERBOSE_MODE = False
  73. def log_msg(msg):
  74. if VERBOSE_MODE:
  75. sys.stdout.write("[b10-zonemgr] %s\n" % str(msg))
  76. class ZonemgrException(Exception):
  77. pass
  78. class ZonemgrRefresh:
  79. """This class will maintain and manage zone refresh info.
  80. It also provides methods to keep track of zone timers and
  81. do zone refresh.
  82. """
  83. def __init__(self, cc, db_file, slave_socket):
  84. self._cc = cc
  85. self._socket = slave_socket
  86. self._db_file = db_file
  87. self._zonemgr_refresh_info = {}
  88. self._build_zonemgr_refresh_info()
  89. def _random_jitter(self, max, jitter):
  90. """Imposes some random jitters for refresh and
  91. retry timers to avoid many zones need to do refresh
  92. at the same time.
  93. The value should be between (max - jitter) and max.
  94. """
  95. if 0 == jitter:
  96. return max
  97. return random.uniform(max - jitter, max)
  98. def _get_current_time(self):
  99. return time.time()
  100. def _set_zone_timer(self, zone_name_class, max, jitter):
  101. """Set zone next refresh time."""
  102. self._set_zone_next_refresh_time(zone_name_class, self._get_current_time() + \
  103. self._random_jitter(max, jitter))
  104. def _set_zone_refresh_timer(self, zone_name_class):
  105. """Set zone next refresh time after zone refresh success.
  106. now + refresh*3/4 <= next_refresh_time <= now + refresh
  107. """
  108. zone_refresh_time = float(self._get_zone_soa_rdata(zone_name_class).split(" ")[REFRESH_OFFSET])
  109. zone_refresh_time = max(LOWERBOUND_REFRESH, zone_refresh_time)
  110. self._set_zone_timer(zone_name_class, zone_refresh_time, (1 * zone_refresh_time) / 4)
  111. def _set_zone_retry_timer(self, zone_name_class):
  112. """Set zone next refresh time after zone refresh fail.
  113. now + retry*3/4 <= next_refresh_time <= now + retry
  114. """
  115. zone_retry_time = float(self._get_zone_soa_rdata(zone_name_class).split(" ")[RETRY_OFFSET])
  116. zone_retry_time = max(LOWERBOUND_RETRY, zone_retry_time)
  117. self._set_zone_timer(zone_name_class, zone_retry_time, (1 * zone_retry_time) / 4)
  118. def _set_zone_notify_timer(self, zone_name_class):
  119. """Set zone next refresh time after receiving notify
  120. next_refresh_time = now
  121. """
  122. self._set_zone_timer(zone_name_class, 0, 0)
  123. def _zone_not_exist(self, zone_name_class):
  124. """ Zone doesn't belong to zonemgr"""
  125. if zone_name_class in self._zonemgr_refresh_info.keys():
  126. return False
  127. return True
  128. def zone_refresh_success(self, zone_name_class):
  129. """Update zone info after zone refresh success"""
  130. if (self._zone_not_exist(zone_name_class)):
  131. raise ZonemgrException("[b10-zonemgr] Zone (%s, %s) doesn't "
  132. "belong to zonemgr" % zone_name_class)
  133. return
  134. self.zonemgr_reload_zone(zone_name_class)
  135. self._set_zone_refresh_timer(zone_name_class)
  136. self._set_zone_state(zone_name_class, ZONE_OK)
  137. self._set_zone_last_refresh_time(zone_name_class, self._get_current_time())
  138. def zone_refresh_fail(self, zone_name_class):
  139. """Update zone info after zone refresh fail"""
  140. if (self._zone_not_exist(zone_name_class)):
  141. raise ZonemgrException("[b10-zonemgr] Zone (%s, %s) doesn't "
  142. "belong to zonemgr" % zone_name_class)
  143. return
  144. self._set_zone_state(zone_name_class, ZONE_OK)
  145. self._set_zone_retry_timer(zone_name_class)
  146. def zone_handle_notify(self, zone_name_class, master):
  147. """Handle zone notify"""
  148. if (self._zone_not_exist(zone_name_class)):
  149. raise ZonemgrException("[b10-zonemgr] Notified zone (%s, %s) "
  150. "doesn't belong to zonemgr" % zone_name_class)
  151. return
  152. self._set_zone_notifier_master(zone_name_class, master)
  153. self._set_zone_notify_timer(zone_name_class)
  154. def zonemgr_reload_zone(self, zone_name_class):
  155. """ Reload a zone."""
  156. zone_soa = sqlite3_ds.get_zone_soa(str(zone_name_class[0]), self._db_file)
  157. self._zonemgr_refresh_info[zone_name_class]["zone_soa_rdata"] = zone_soa[7]
  158. def zonemgr_add_zone(self, zone_name_class):
  159. """ Add a zone into zone manager."""
  160. zone_info = {}
  161. zone_soa = sqlite3_ds.get_zone_soa(str(zone_name_class[0]), self._db_file)
  162. if not zone_soa:
  163. raise ZonemgrException("[b10-zonemgr] zone (%s, %s) doesn't have soa." % zone_name_class)
  164. zone_info["zone_soa_rdata"] = zone_soa[7]
  165. zone_info["zone_state"] = ZONE_OK
  166. zone_info["last_refresh_time"] = self._get_current_time()
  167. zone_info["next_refresh_time"] = self._get_current_time() + \
  168. float(zone_soa[7].split(" ")[REFRESH_OFFSET])
  169. self._zonemgr_refresh_info[zone_name_class] = zone_info
  170. def _build_zonemgr_refresh_info(self):
  171. """ Build zonemgr refresh info map."""
  172. log_msg("Start loading zone into zonemgr.")
  173. for zone_name, zone_class in sqlite3_ds.get_zones_info(self._db_file):
  174. zone_name_class = (zone_name, zone_class)
  175. self.zonemgr_add_zone(zone_name_class)
  176. log_msg("Finish loading zone into zonemgr.")
  177. def _zone_is_expired(self, zone_name_class):
  178. """Judge whether a zone is expired or not."""
  179. zone_expired_time = float(self._get_zone_soa_rdata(zone_name_class).split(" ")[EXPIRED_OFFSET])
  180. zone_last_refresh_time = self._get_zone_last_refresh_time(zone_name_class)
  181. if (ZONE_EXPIRED == self._get_zone_state(zone_name_class) or
  182. zone_last_refresh_time + zone_expired_time <= self._get_current_time()):
  183. return True
  184. return False
  185. def _get_zone_soa_rdata(self, zone_name_class):
  186. return self._zonemgr_refresh_info[zone_name_class]["zone_soa_rdata"]
  187. def _get_zone_last_refresh_time(self, zone_name_class):
  188. return self._zonemgr_refresh_info[zone_name_class]["last_refresh_time"]
  189. def _set_zone_last_refresh_time(self, zone_name_class, time):
  190. self._zonemgr_refresh_info[zone_name_class]["last_refresh_time"] = time
  191. def _get_zone_notifier_master(self, zone_name_class):
  192. if ("notify_master" in self._zonemgr_refresh_info[zone_name_class].keys()):
  193. return self._zonemgr_refresh_info[zone_name_class]["notify_master"]
  194. return None
  195. def _set_zone_notifier_master(self, zone_name_class, master_addr):
  196. self._zonemgr_refresh_info[zone_name_class]["notify_master"] = master_addr
  197. def _clear_zone_notifier_master(self, zone_name_class):
  198. if ("notify_master" in self._zonemgr_refresh_info[zone_name_class].keys()):
  199. del self._zonemgr_refresh_info[zone_name_class]["notify_master"]
  200. def _get_zone_state(self, zone_name_class):
  201. return self._zonemgr_refresh_info[zone_name_class]["zone_state"]
  202. def _set_zone_state(self, zone_name_class, zone_state):
  203. self._zonemgr_refresh_info[zone_name_class]["zone_state"] = zone_state
  204. def _get_zone_refresh_timeout(self, zone_name_class):
  205. return self._zonemgr_refresh_info[zone_name_class]["refresh_timeout"]
  206. def _set_zone_refresh_timeout(self, zone_name_class, time):
  207. self._zonemgr_refresh_info[zone_name_class]["refresh_timeout"] = time
  208. def _get_zone_next_refresh_time(self, zone_name_class):
  209. return self._zonemgr_refresh_info[zone_name_class]["next_refresh_time"]
  210. def _set_zone_next_refresh_time(self, zone_name_class, time):
  211. self._zonemgr_refresh_info[zone_name_class]["next_refresh_time"] = time
  212. def _send_command(self, module_name, command_name, params):
  213. """Send command between modules."""
  214. msg = create_command(command_name, params)
  215. try:
  216. self._cc.group_sendmsg(msg, module_name)
  217. except socket.error:
  218. sys.stderr.write("[b10-zonemgr] Failed to send to module %s, the session has been closed." % module_name)
  219. def _find_need_do_refresh_zone(self):
  220. """Find the first zone need do refresh, if no zone need
  221. do refresh, return the zone with minimum next_refresh_time.
  222. """
  223. zone_need_refresh = None
  224. for zone_name_class in self._zonemgr_refresh_info.keys():
  225. # Does the zone expired?
  226. if (ZONE_EXPIRED != self._get_zone_state(zone_name_class) and
  227. self._zone_is_expired(zone_name_class)):
  228. log_msg("Zone (%s, %s) is expired." % zone_name_class)
  229. self._set_zone_state(zone_name_class, ZONE_EXPIRED)
  230. zone_state = self._get_zone_state(zone_name_class)
  231. # If zone is expired and doesn't receive notify, skip the zone
  232. if (ZONE_EXPIRED == zone_state and
  233. (not self._get_zone_notifier_master(zone_name_class))):
  234. continue
  235. # If hasn't received refresh response but are within refresh timeout, skip the zone
  236. if (ZONE_REFRESHING == zone_state and
  237. (self._get_zone_refresh_timeout(zone_name_class) > self._get_current_time())):
  238. continue
  239. # Get the zone with minimum next_refresh_time
  240. if ((None == zone_need_refresh) or
  241. (self._get_zone_next_refresh_time(zone_name_class) <
  242. self._get_zone_next_refresh_time(zone_need_refresh))):
  243. zone_need_refresh = zone_name_class
  244. # Find the zone need do refresh
  245. if (self._get_zone_next_refresh_time(zone_need_refresh) < self._get_current_time()):
  246. break
  247. return zone_need_refresh
  248. def _do_refresh(self, zone_name_class):
  249. """Do zone refresh."""
  250. log_msg("Do refresh for zone (%s, %s)." % zone_name_class)
  251. self._set_zone_state(zone_name_class, ZONE_REFRESHING)
  252. self._set_zone_refresh_timeout(zone_name_class, self._get_current_time() + MAX_TRANSFER_TIMEOUT)
  253. notify_master = self._get_zone_notifier_master(zone_name_class)
  254. # If the zone has notify master, send notify command to xfrin module
  255. if notify_master:
  256. param = {"zone_name" : zone_name_class[0],
  257. "zone_class" : zone_name_class[1],
  258. "master" : notify_master
  259. }
  260. self._send_command(XFRIN_MODULE_NAME, ZONE_NOTIFY_COMMAND, param)
  261. self._clear_zone_notifier_master(zone_name_class)
  262. # Send refresh command to xfrin module
  263. else:
  264. param = {"zone_name" : zone_name_class[0],
  265. "zone_class" : zone_name_class[1]
  266. }
  267. self._send_command(XFRIN_MODULE_NAME, ZONE_REFRESH_COMMAND, param)
  268. def _zone_mgr_is_empty(self):
  269. """Does zone manager has no zone?"""
  270. if not len(self._zonemgr_refresh_info):
  271. return True
  272. return False
  273. def run_timer(self):
  274. """Keep track of zone timers."""
  275. while True:
  276. # Zonemgr has no zone.
  277. if self._zone_mgr_is_empty():
  278. time.sleep(LOWERBOUND_RETRY) # A better time?
  279. continue
  280. zone_need_refresh = self._find_need_do_refresh_zone()
  281. # If don't get zone with minimum next refresh time, set timer timeout = LOWERBOUND_REFRESH
  282. if not zone_need_refresh:
  283. timeout = LOWERBOUND_RETRY
  284. else:
  285. timeout = self._get_zone_next_refresh_time(zone_need_refresh) - self._get_current_time()
  286. if (timeout < 0):
  287. self._do_refresh(zone_need_refresh)
  288. continue
  289. """ Wait for the socket notification for a maximum time of timeout
  290. in seconds (as float)."""
  291. try:
  292. (rlist, wlist, xlist) = select.select([self._socket], [], [], timeout)
  293. if rlist:
  294. self._socket.recv(32)
  295. except ValueError as e:
  296. raise ZonemgrException("[b10-zonemgr] Socket has been closed\n")
  297. break
  298. except select.error as e:
  299. if e.args[0] == errno.EINTR:
  300. (rlist, wlist, xlist) = ([], [], [])
  301. else:
  302. raise ZonemgrException("[b10-zonemgr] Error with select(): %s\n" % e)
  303. break
  304. class Zonemgr:
  305. """Zone manager class."""
  306. def __init__(self):
  307. self._setup_session()
  308. self._db_file = self.get_db_file()
  309. # Create socket pair for communicating between main thread and zonemgr timer thread
  310. self._master_socket, self._slave_socket = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
  311. self._zone_refresh= ZonemgrRefresh(self._cc, self._db_file, self._slave_socket)
  312. self._start_zone_refresh_timer()
  313. self._lock = threading.Lock()
  314. self._shutdown_event = threading.Event()
  315. def _start_zone_refresh_timer(self):
  316. """Start a new thread to keep track of zone timers"""
  317. listener = threading.Thread(target = self._zone_refresh.run_timer, args = ())
  318. listener.setDaemon(True)
  319. listener.start()
  320. def _setup_session(self):
  321. """Setup two sessions for zonemgr, one(self._module_cc) is used for receiving
  322. commands and config data sent from other modules, another one (self._cc)
  323. is used to send commands to proper modules."""
  324. self._cc = isc.cc.Session()
  325. self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
  326. self.config_handler,
  327. self.command_handler)
  328. self._module_cc.add_remote_config(AUTH_SPECFILE_LOCATION)
  329. self._config_data = self._module_cc.get_full_config()
  330. self._module_cc.start()
  331. def get_db_file(self):
  332. db_file, is_default = self._module_cc.get_remote_config_value(AUTH_MODULE_NAME, "database_file")
  333. # this too should be unnecessary, but currently the
  334. # 'from build' override isn't stored in the config
  335. # (and we don't have indirect python access to datasources yet)
  336. if is_default and "B10_FROM_BUILD" in os.environ:
  337. db_file = os.environ["B10_FROM_BUILD"] + "/bind10_zones.sqlite3"
  338. return db_file
  339. def shutdown(self):
  340. """Shutdown the zonemgr process. the thread which is keeping track of zone
  341. timers should be terminated.
  342. """
  343. self._slave_socket.close()
  344. self._master_socket.close()
  345. self._shutdown_event.set()
  346. main_thread = threading.currentThread()
  347. for th in threading.enumerate():
  348. if th is main_thread:
  349. continue
  350. th.join()
  351. def config_handler(self, new_config):
  352. """Update config data."""
  353. answer = create_answer(0)
  354. for key in new_config:
  355. if key not in self._config_data:
  356. answer = create_answer(1, "Unknown config data: " + str(key))
  357. continue
  358. self._config_data[key] = new_config[key]
  359. return answer
  360. def _parse_cmd_params(self, args, command):
  361. zone_name = args.get("zone_name")
  362. if not zone_name:
  363. raise ZonemgrException("zone name should be provided")
  364. zone_class = args.get("zone_class")
  365. if not zone_class:
  366. raise ZonemgrException("zone class should be provided")
  367. if (command != ZONE_NOTIFY_COMMAND):
  368. return (zone_name, zone_class)
  369. master_str = args.get("master")
  370. if not master_str:
  371. raise ZonemgrException("master address should be provided")
  372. return ((zone_name, zone_class), master_str)
  373. def command_handler(self, command, args):
  374. """Handle command receivd from command channel.
  375. ZONE_NOTIFY_COMMAND is issued by Auth process; ZONE_XFRIN_SUCCESS_COMMAND
  376. and ZONE_XFRIN_FAILED_COMMAND are issued by Xfrin process; shutdown is issued
  377. by a user or Boss process. """
  378. answer = create_answer(0)
  379. if command == ZONE_NOTIFY_COMMAND:
  380. """ Handle Auth notify command"""
  381. # master is the source sender of the notify message.
  382. zone_name_class, master = self._parse_cmd_params(args, command)
  383. log_msg("Received notify command for zone (%s, %s)." % zone_name_class)
  384. with self._lock:
  385. self._zone_refresh.zone_handle_notify(zone_name_class, master)
  386. # Send notification to zonemgr timer thread
  387. self._master_socket.send(b" ")
  388. elif command == ZONE_XFRIN_SUCCESS_COMMAND:
  389. """ Handle xfrin success command"""
  390. zone_name_class = self._parse_cmd_params(args, command)
  391. with self._lock:
  392. self._zone_refresh.zone_refresh_success(zone_name_class)
  393. self._master_socket.send(b" ")
  394. elif command == ZONE_XFRIN_FAILED_COMMAND:
  395. """ Handle xfrin fail command"""
  396. zone_name_class = self._parse_cmd_params(args, command)
  397. with self._lock:
  398. self._zone_refresh.zone_refresh_fail(zone_name_class)
  399. self._master_socket.send(b" ")
  400. elif command == "shutdown":
  401. self.shutdown()
  402. else:
  403. answer = create_answer(1, "Unknown command:" + str(command))
  404. return answer
  405. def run(self):
  406. while not self._shutdown_event.is_set():
  407. self._module_cc.check_command()
  408. zonemgrd = None
  409. def signal_handler(signal, frame):
  410. if zonemgrd:
  411. zonemgrd.shutdown()
  412. sys.exit(0)
  413. def set_signal_handler():
  414. signal.signal(signal.SIGTERM, signal_handler)
  415. signal.signal(signal.SIGINT, signal_handler)
  416. def set_cmd_options(parser):
  417. parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
  418. help="display more about what is going on")
  419. if '__main__' == __name__:
  420. try:
  421. parser = OptionParser()
  422. set_cmd_options(parser)
  423. (options, args) = parser.parse_args()
  424. VERBOSE_MODE = options.verbose
  425. set_signal_handler()
  426. zonemgrd = Zonemgr()
  427. zonemgrd.run()
  428. except KeyboardInterrupt:
  429. sys.stderr.write("[b10-zonemgr] exit zonemgr process\n")
  430. except isc.cc.session.SessionError as e:
  431. sys.stderr.write("[b10-zonemgr] Error creating zonemgr, "
  432. "is the command channel daemon running?\n")
  433. except isc.cc.session.SessionTimeout as e:
  434. sys.stderr.write("[b10-zonemgr] Error creating zonemgr, "
  435. "is the configuration manager running?\n")
  436. except isc.config.ModuleCCSessionError as e:
  437. sys.stderr.write("[b10-zonemgr] exit zonemgr process: %s\n" % str(e))
  438. if zonemgrd:
  439. zonemgrd.shutdown()