loadzone.py.in 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. #!@PYTHON@
  2. # Copyright (C) 2012 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. import sys
  17. sys.path.append('@@PYTHONPATH@@')
  18. import time
  19. import signal
  20. from optparse import OptionParser
  21. from isc.dns import *
  22. from isc.datasrc import *
  23. import isc.util.process
  24. import isc.util.traceback_handler
  25. import isc.log
  26. from isc.log_messages.loadzone_messages import *
  27. from datetime import timedelta
  28. isc.util.process.rename()
  29. # These are needed for logger settings
  30. import bind10_config
  31. import json
  32. from isc.config import module_spec_from_file
  33. from isc.config.ccsession import path_search
  34. isc.log.init("b10-loadzone")
  35. logger = isc.log.Logger("loadzone")
  36. # The default value for the interval of progress report in terms of the
  37. # number of RRs loaded in that interval. Arbitrary choice, but intended to
  38. # be reasonably small to handle emergency exit.
  39. LOAD_INTERVAL_DEFAULT = 10000
  40. class BadArgument(Exception):
  41. '''An exception indicating an error in command line argument.
  42. '''
  43. pass
  44. class LoadFailure(Exception):
  45. '''An exception indicating failure in loading operation.
  46. '''
  47. pass
  48. def set_cmd_options(parser):
  49. '''Helper function to set command-line options.
  50. '''
  51. parser.add_option("-c", "--datasrc-conf", dest="conf", action="store",
  52. help="""configuration of datasrc to load the zone in.
  53. Example: '{"database_file": "/path/to/dbfile/db.sqlite3"}'""",
  54. metavar='CONFIG')
  55. parser.add_option("-d", "--debug", dest="debug_level",
  56. type='int', action="store", default=None,
  57. help="enable debug logs with the specified level [0-99]")
  58. parser.add_option("-e", "--empty", dest="empty_zone",
  59. action="store_true", help="empty zone content (no load)")
  60. parser.add_option("-i", "--report-interval", dest="report_interval",
  61. type='int', action="store",
  62. default=LOAD_INTERVAL_DEFAULT,
  63. help="""report logs progress per specified number of RRs
  64. (specify 0 to suppress report) [default: %default]""")
  65. parser.add_option("-t", "--datasrc-type", dest="datasrc_type",
  66. action="store", default='sqlite3',
  67. help="""type of data source (e.g., 'sqlite3')\n
  68. [default: %default]""")
  69. parser.add_option("-C", "--class", dest="zone_class", action="store",
  70. default='IN',
  71. help="""RR class of the zone; currently must be 'IN'
  72. [default: %default]""")
  73. class LoadZoneRunner:
  74. '''Main logic for the loadzone.
  75. This is implemented as a class mainly for the convenience of tests.
  76. '''
  77. def __init__(self, command_args):
  78. self.__command_args = command_args
  79. self.__interrupted = False # will be set to True on receiving signal
  80. # system-wide log configuration. We need to configure logging this
  81. # way so that the logging policy applies to underlying libraries, too.
  82. self.__log_spec = json.dumps(isc.config.module_spec_from_file(
  83. path_search('logging.spec', bind10_config.PLUGIN_PATHS)).
  84. get_full_spec())
  85. # "severity" and "debuglevel" are the tunable parameters, which will
  86. # be set in _config_log().
  87. self.__log_conf_base = {"loggers":
  88. [{"name": "*",
  89. "output_options":
  90. [{"output": "stderr",
  91. "destination": "console"}]}]}
  92. # These are essentially private, but defined as "protected" for the
  93. # convenience of tests inspecting them
  94. self._loaded_rrs = 0
  95. self._zone_class = None
  96. self._zone_name = None
  97. self._zone_file = None
  98. self._datasrc_config = None
  99. self._datasrc_type = None
  100. self._log_severity = 'INFO'
  101. self._log_debuglevel = 0
  102. self._empty_zone = False
  103. self._report_interval = LOAD_INTERVAL_DEFAULT
  104. self._start_time = None
  105. # This one will be used in (rare) cases where we want to allow tests to
  106. # fake time.time()
  107. self._get_time = time.time
  108. self._config_log()
  109. def _config_log(self):
  110. '''Configure logging policy.
  111. This is essentially private, but defined as "protected" for tests.
  112. '''
  113. self.__log_conf_base['loggers'][0]['severity'] = self._log_severity
  114. self.__log_conf_base['loggers'][0]['debuglevel'] = self._log_debuglevel
  115. isc.log.log_config_update(json.dumps(self.__log_conf_base),
  116. self.__log_spec)
  117. def _parse_args(self):
  118. '''Parse command line options and other arguments.
  119. This is essentially private, but defined as "protected" for tests.
  120. '''
  121. usage_txt = \
  122. 'usage: %prog [options] -c datasrc_config zonename zonefile\n' + \
  123. ' %prog [options] -c datasrc_config -e zonename'
  124. parser = OptionParser(usage=usage_txt)
  125. set_cmd_options(parser)
  126. (options, args) = parser.parse_args(args=self.__command_args)
  127. # Configure logging policy as early as possible
  128. if options.debug_level is not None:
  129. self._log_severity = 'DEBUG'
  130. # optparse performs type check
  131. self._log_debuglevel = int(options.debug_level)
  132. if self._log_debuglevel < 0:
  133. raise BadArgument(
  134. 'Invalid debug level (must be non negative): %d' %
  135. self._log_debuglevel)
  136. self._config_log()
  137. self._datasrc_type = options.datasrc_type
  138. self._datasrc_config = options.conf
  139. if options.conf is None:
  140. self._datasrc_config = self._get_datasrc_config(self._datasrc_type)
  141. try:
  142. self._zone_class = RRClass(options.zone_class)
  143. except isc.dns.InvalidRRClass as ex:
  144. raise BadArgument('Invalid zone class: ' + str(ex))
  145. if self._zone_class != RRClass.IN:
  146. raise BadArgument("RR class is not supported: " +
  147. str(self._zone_class))
  148. self._report_interval = int(options.report_interval)
  149. if self._report_interval < 0:
  150. raise BadArgument(
  151. 'Invalid report interval (must be non negative): %d' %
  152. self._report_interval)
  153. if options.empty_zone:
  154. self._empty_zone = True
  155. # Check number of non option arguments: must be 1 with -e; 2 otherwise.
  156. num_args = 1 if self._empty_zone else 2
  157. if len(args) != num_args:
  158. raise BadArgument('Unexpected number of arguments: %d (must be %d)'
  159. % (len(args), num_args))
  160. try:
  161. self._zone_name = Name(args[0])
  162. except Exception as ex: # too broad, but there's no better granurality
  163. raise BadArgument("Invalid zone name '" + args[0] + "': " +
  164. str(ex))
  165. if len(args) > 1:
  166. self._zone_file = args[1]
  167. def _get_datasrc_config(self, datasrc_type):
  168. ''''Return the default data source configuration of given type.
  169. Right now, it only supports SQLite3, and hardcodes the syntax
  170. of the default configuration. It's a kind of workaround to balance
  171. convenience of users and minimizing hardcoding of data source
  172. specific logic in the entire tool. In future this should be
  173. more sophisticated.
  174. This is essentially a private helper method for _parse_arg(),
  175. but defined as "protected" so tests can use it directly.
  176. '''
  177. if datasrc_type != 'sqlite3':
  178. raise BadArgument('default config is not available for ' +
  179. datasrc_type)
  180. default_db_file = bind10_config.DATA_PATH + '/zone.sqlite3'
  181. logger.info(LOADZONE_SQLITE3_USING_DEFAULT_CONFIG, default_db_file)
  182. return '{"database_file": "' + default_db_file + '"}'
  183. def _report_progress(self, loaded_rrs, progress, dump=True):
  184. '''Dump the current progress report to stdout.
  185. This is essentially private, but defined as "protected" for tests.
  186. Normally dump is True, but tests will set it False to get the
  187. text to be reported. Tests may also fake self._get_time (which
  188. is set to time.time() by default) and self._start_time for control
  189. time related conditions.
  190. '''
  191. elapsed = self._get_time() - self._start_time
  192. speed = int(loaded_rrs / elapsed) if elapsed > 0 else 0
  193. etc = None # calculate estimated time of completion
  194. if progress != ZoneLoader.PROGRESS_UNKNOWN:
  195. etc = (1 - progress) * (elapsed / progress)
  196. # Build report text
  197. report_txt = '\r%d RRs' % loaded_rrs
  198. if progress != ZoneLoader.PROGRESS_UNKNOWN:
  199. report_txt += ' (%.1f%%)' % (progress * 100)
  200. report_txt += ' in %s, %d RRs/sec' % \
  201. (str(timedelta(seconds=int(elapsed))), speed)
  202. if etc is not None:
  203. report_txt += ', %s ETC' % str(timedelta(seconds=int(etc)))
  204. # Dump or return the report text.
  205. if dump:
  206. sys.stdout.write("\r" + (80 * " "))
  207. sys.stdout.write(report_txt)
  208. else:
  209. return report_txt
  210. def _do_load(self):
  211. '''Main part of the load logic.
  212. This is essentially private, but defined as "protected" for tests.
  213. '''
  214. created = False
  215. try:
  216. datasrc_client = DataSourceClient(self._datasrc_type,
  217. self._datasrc_config)
  218. created = datasrc_client.create_zone(self._zone_name)
  219. if created:
  220. logger.info(LOADZONE_ZONE_CREATED, self._zone_name,
  221. self._zone_class)
  222. else:
  223. logger.info(LOADZONE_ZONE_UPDATING, self._zone_name,
  224. self._zone_class)
  225. if self._empty_zone:
  226. self.__make_empty_zone(datasrc_client)
  227. else:
  228. self.__load_from_file(datasrc_client)
  229. except Exception as ex:
  230. if created:
  231. datasrc_client.delete_zone(self._zone_name)
  232. logger.error(LOADZONE_CANCEL_CREATE_ZONE, self._zone_name,
  233. self._zone_class)
  234. raise LoadFailure(str(ex))
  235. def __make_empty_zone(self, datasrc_client):
  236. """Subroutine of _do_load(), create an empty zone or make it empty."""
  237. try:
  238. updater = datasrc_client.get_updater(self._zone_name, True)
  239. updater.commit()
  240. logger.info(LOADZONE_EMPTY_DONE, self._zone_name,
  241. self._zone_class)
  242. except Exception:
  243. # once updater is created, it's very unlikely that commit() fails,
  244. # but in case it happens, clear updater to release any remaining
  245. # lock.
  246. updater = None
  247. raise
  248. def __load_from_file(self, datasrc_client):
  249. """Subroutine of _do_load(), load a zone file into data source."""
  250. try:
  251. loader = ZoneLoader(datasrc_client, self._zone_name,
  252. self._zone_file)
  253. self._start_time = time.time()
  254. if self._report_interval > 0:
  255. limit = self._report_interval
  256. else:
  257. # Even if progress report is suppressed, we still load
  258. # incrementally so we won't delay catching signals too long.
  259. limit = LOAD_INTERVAL_DEFAULT
  260. while (not self.__interrupted and
  261. not loader.load_incremental(limit)):
  262. self._loaded_rrs += self._report_interval
  263. if self._report_interval > 0:
  264. self._report_progress(self._loaded_rrs,
  265. loader.get_progress())
  266. if self.__interrupted:
  267. raise LoadFailure('loading interrupted by signal')
  268. # On successful completion, add final '\n' to the progress
  269. # report output (on failure don't bother to make it prettier).
  270. if (self._report_interval > 0 and
  271. self._loaded_rrs >= self._report_interval):
  272. sys.stdout.write('\n')
  273. # record the final count of the loaded RRs for logging
  274. self._loaded_rrs = loader.get_rr_count()
  275. total_elapsed_txt = "%.2f" % (time.time() - self._start_time)
  276. logger.info(LOADZONE_DONE, self._loaded_rrs, self._zone_name,
  277. self._zone_class, total_elapsed_txt)
  278. except Exception:
  279. # release any remaining lock held in the loader
  280. loader = None
  281. raise
  282. def _set_signal_handlers(self):
  283. signal.signal(signal.SIGINT, self._interrupt_handler)
  284. signal.signal(signal.SIGTERM, self._interrupt_handler)
  285. def _interrupt_handler(self, signal, frame):
  286. self.__interrupted = True
  287. def run(self):
  288. '''Top-level method, simply calling other helpers'''
  289. try:
  290. self._set_signal_handlers()
  291. self._parse_args()
  292. self._do_load()
  293. return 0
  294. except BadArgument as ex:
  295. logger.error(LOADZONE_ARGUMENT_ERROR, ex)
  296. except LoadFailure as ex:
  297. logger.error(LOADZONE_LOAD_ERROR, self._zone_name,
  298. self._zone_class, ex)
  299. except Exception as ex:
  300. logger.error(LOADZONE_UNEXPECTED_FAILURE, ex)
  301. return 1
  302. def main():
  303. runner = LoadZoneRunner(sys.argv[1:])
  304. ret = runner.run()
  305. sys.exit(ret)
  306. if '__main__' == __name__:
  307. isc.util.traceback_handler.traceback_handler(main)
  308. ## Local Variables:
  309. ## mode: python
  310. ## End: