bind10_control.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. # Copyright (C) 2011-2012 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. from lettuce import *
  16. import time
  17. import subprocess
  18. import re
  19. import json
  20. @step('sleep for (\d+) seconds')
  21. def wait_seconds(step, seconds):
  22. """Sleep for some seconds.
  23. Parameters:
  24. seconds number of seconds to sleep for.
  25. """
  26. time.sleep(float(seconds))
  27. @step('start bind10(?: with configuration (\S+))?' +\
  28. '(?: with cmdctl port (\d+))?' +\
  29. '(?: with msgq socket file (\S+))?' +\
  30. '(?: as (\S+))?')
  31. def start_bind10(step, config_file, cmdctl_port, msgq_sockfile, process_name):
  32. """
  33. Start BIND 10 with the given optional config file, cmdctl port, and
  34. store the running process in world with the given process name.
  35. Parameters:
  36. config_file ('with configuration <file>', optional): this configuration
  37. will be used. The path is relative to the base lettuce
  38. directory.
  39. cmdctl_port ('with cmdctl port <portnr>', optional): The port on which
  40. b10-cmdctl listens for bindctl commands. Defaults to 47805.
  41. msgq_sockfile ('with msgq socket file', optional): The msgq socket file
  42. that will be used for internal communication
  43. process_name ('as <name>', optional). This is the name that can be used
  44. in the following steps of the scenario to refer to this
  45. BIND 10 instance. Defaults to 'bind10'.
  46. This call will block until BIND10_STARTUP_COMPLETE or BIND10_STARTUP_ERROR
  47. is logged. In the case of the latter, or if it times out, the step (and
  48. scenario) will fail.
  49. It will also fail if there is a running process with the given process_name
  50. already.
  51. """
  52. args = [ 'bind10', '-v' ]
  53. if config_file is not None:
  54. args.append('-p')
  55. args.append("configurations/")
  56. args.append('-c')
  57. args.append(config_file)
  58. if cmdctl_port is None:
  59. args.append('--cmdctl-port=47805')
  60. else:
  61. args.append('--cmdctl-port=' + cmdctl_port)
  62. if process_name is None:
  63. process_name = "bind10"
  64. else:
  65. args.append('-m')
  66. args.append(process_name + '_msgq.socket')
  67. world.processes.add_process(step, process_name, args)
  68. # check output to know when startup has been completed
  69. (message, line) = world.processes.wait_for_stderr_str(process_name,
  70. ["BIND10_STARTUP_COMPLETE",
  71. "BIND10_STARTUP_ERROR"])
  72. assert message == "BIND10_STARTUP_COMPLETE", "Got: " + str(line)
  73. @step('wait for bind10 auth (?:of (\w+) )?to start')
  74. def wait_for_auth(step, process_name):
  75. """Wait for b10-auth to run. This is done by blocking until the message
  76. AUTH_SERVER_STARTED is logged.
  77. Parameters:
  78. process_name ('of <name', optional): The name of the BIND 10 instance
  79. to wait for. Defaults to 'bind10'.
  80. """
  81. if process_name is None:
  82. process_name = "bind10"
  83. world.processes.wait_for_stderr_str(process_name, ['AUTH_SERVER_STARTED'],
  84. False)
  85. @step('wait for bind10 xfrout (?:of (\w+) )?to start')
  86. def wait_for_xfrout(step, process_name):
  87. """Wait for b10-xfrout to run. This is done by blocking until the message
  88. XFROUT_NEW_CONFIG_DONE is logged.
  89. Parameters:
  90. process_name ('of <name', optional): The name of the BIND 10 instance
  91. to wait for. Defaults to 'bind10'.
  92. """
  93. if process_name is None:
  94. process_name = "bind10"
  95. world.processes.wait_for_stderr_str(process_name,
  96. ['XFROUT_NEW_CONFIG_DONE'],
  97. False)
  98. @step('have bind10 running(?: with configuration ([\S]+))?' +\
  99. '(?: with cmdctl port (\d+))?' +\
  100. '(?: as ([\S]+))?')
  101. def have_bind10_running(step, config_file, cmdctl_port, process_name):
  102. """
  103. Compound convenience step for running bind10, which consists of
  104. start_bind10.
  105. Currently only supports the 'with configuration' option.
  106. """
  107. start_step = 'start bind10 with configuration ' + config_file
  108. if cmdctl_port is not None:
  109. start_step += ' with cmdctl port ' + str(cmdctl_port)
  110. if process_name is not None:
  111. start_step += ' as ' + process_name
  112. step.given(start_step)
  113. # function to send lines to bindctl, and store the result
  114. def run_bindctl(commands, cmdctl_port=None, ignore_failure=False):
  115. """Run bindctl.
  116. Parameters:
  117. commands: a sequence of strings which will be sent.
  118. cmdctl_port: a port number on which cmdctl is listening, is converted
  119. to string if necessary. If not provided, or None, defaults
  120. to 47805
  121. bindctl's stdout and stderr streams are stored (as one multiline string
  122. in world.last_bindctl_stdout/stderr.
  123. Fails if the return code is not 0
  124. """
  125. if cmdctl_port is None:
  126. cmdctl_port = 47805
  127. args = ['bindctl', '-p', str(cmdctl_port)]
  128. bindctl = subprocess.Popen(args, 1, None, subprocess.PIPE,
  129. subprocess.PIPE, None)
  130. for line in commands:
  131. bindctl.stdin.write(line + "\n")
  132. (stdout, stderr) = bindctl.communicate()
  133. if ignore_failure:
  134. return
  135. result = bindctl.returncode
  136. world.last_bindctl_stdout = stdout
  137. world.last_bindctl_stderr = stderr
  138. assert result == 0, "bindctl exit code: " + str(result) +\
  139. "\nstdout:\n" + str(stdout) +\
  140. "stderr:\n" + str(stderr)
  141. @step('last bindctl( stderr)? output should( not)? contain (\S+)( exactly)?')
  142. def check_bindctl_output(step, stderr, notv, string, exactly):
  143. """Checks the stdout (or stderr) stream of the last run of bindctl,
  144. fails if the given string is not found in it (or fails if 'not' was
  145. set and it is found
  146. Parameters:
  147. stderr ('stderr'): Check stderr instead of stdout output
  148. notv ('not'): reverse the check (fail if string is found)
  149. string ('contain <string>') string to look for
  150. exactly ('exactly'): Make an exact match delimited by whitespace
  151. """
  152. if stderr is None:
  153. output = world.last_bindctl_stdout
  154. else:
  155. output = world.last_bindctl_stderr
  156. found = False
  157. if exactly is None:
  158. if string in output:
  159. found = True
  160. else:
  161. if re.search(r'^\s+' + string + r'\s+', output, re.IGNORECASE | re.MULTILINE) is not None:
  162. found = True
  163. if notv is None:
  164. assert found == True, "'" + string +\
  165. "' was not found in bindctl output:\n" +\
  166. output
  167. else:
  168. assert not found, "'" + string +\
  169. "' was found in bindctl output:\n" +\
  170. output
  171. def parse_bindctl_output_as_data_structure():
  172. """Helper function for data-related command tests: evaluates the
  173. last output of bindctl as a data structure that can then be
  174. inspected.
  175. If the bindctl output is not valid (json) data, this call will
  176. fail with an assertion failure.
  177. If it is valid, it is parsed and returned as whatever data
  178. structure it represented.
  179. """
  180. # strip any extra output after a character that commonly terminates a valid
  181. # JSON expression, i.e., ']', '}' and '"'. (The extra output would
  182. # contain 'Exit from bindctl' message, and depending on environment some
  183. # other control-like characters...but why is this message even there?)
  184. # Note that this filter is not perfect. For example, it cannot recognize
  185. # a simple expression of true/false/null.
  186. output = re.sub("(.*)([^]}\"]*$)", r"\1", world.last_bindctl_stdout)
  187. try:
  188. return json.loads(output)
  189. except ValueError as ve:
  190. assert False, "Last bindctl output does not appear to be a " +\
  191. "parseable data structure: '" + output + "': " + str(ve)
  192. def find_process_pid(step, process_name):
  193. """Helper function to request the running processes from Init, and
  194. return the pid of the process with the given process_name.
  195. Fails with an assert if the response from b10-init is not valid JSON,
  196. or if the process with the given name is not found.
  197. """
  198. # show_processes output is a list of lists, where the inner lists
  199. # are of the form [ pid, "name" ]
  200. # Not checking data form; errors will show anyway (if these turn
  201. # out to be too vague, we can change this)
  202. step.given('send bind10 the command Init show_processes')
  203. running_processes = parse_bindctl_output_as_data_structure()
  204. for process in running_processes:
  205. if process[1] == process_name:
  206. return process[0]
  207. assert False, "Process named " + process_name +\
  208. " not found in output of Init show_processes";
  209. @step("remember the pid of process ([\S]+)")
  210. def remember_pid(step, process_name):
  211. """Stores the PID of the process with the given name as returned by
  212. Init show_processes command.
  213. Fails if the process with the given name does not appear to exist.
  214. Stores the component_name->pid value in the dict world.process_pids.
  215. This should only be used by the related step
  216. 'the pid of process <name> should (not) have changed'
  217. Arguments:
  218. process name ('process <name>') the name of the component to store
  219. the pid of.
  220. """
  221. if world.process_pids is None:
  222. world.process_pids = {}
  223. world.process_pids[process_name] = find_process_pid(step, process_name)
  224. @step('pid of process ([\S]+) should not have changed')
  225. def check_pid(step, process_name):
  226. """Checks the PID of the process with the given name as returned by
  227. Init show_processes command.
  228. Fails if the process with the given name does not appear to exist.
  229. Fails if the process with the given name exists, but has a different
  230. pid than it had when the step 'remember the pid of process' was
  231. called.
  232. Fails if that step has not been called (since world.process_pids
  233. does not exist).
  234. """
  235. assert world.process_pids is not None, "No process pids stored"
  236. assert process_name in world.process_pids, "Process named " +\
  237. process_name +\
  238. " was not stored"
  239. pid = find_process_pid(step, process_name)
  240. assert world.process_pids[process_name] == pid,\
  241. "Expected pid: " + str(world.process_pids[process_name]) +\
  242. " Got pid: " + str(pid)
  243. @step('set bind10 configuration (\S+) to (.*)(?: with cmdctl port (\d+))?')
  244. def config_set_command(step, name, value, cmdctl_port):
  245. """
  246. Run bindctl, set the given configuration to the given value, and commit it.
  247. Parameters:
  248. name ('configuration <name>'): Identifier of the configuration to set
  249. value ('to <value>'): value to set it to.
  250. cmdctl_port ('with cmdctl port <portnr>', optional): cmdctl port to send
  251. the command to. Defaults to 47805.
  252. Fails if cmdctl does not exit with status code 0.
  253. """
  254. commands = ["config set " + name + " " + value,
  255. "config commit",
  256. "quit"]
  257. run_bindctl(commands, cmdctl_port)
  258. @step('send bind10 the following commands(?: with cmdctl port (\d+))?')
  259. def send_multiple_commands(step, cmdctl_port):
  260. """
  261. Run bindctl, and send it the given multiline set of commands.
  262. A quit command is always appended.
  263. cmdctl_port ('with cmdctl port <portnr>', optional): cmdctl port to send
  264. the command to. Defaults to 47805.
  265. Fails if cmdctl does not exit with status code 0.
  266. """
  267. commands = step.multiline.split("\n")
  268. # Always add quit
  269. commands.append("quit")
  270. run_bindctl(commands, cmdctl_port)
  271. @step('remove bind10 configuration (\S+)(?: value (\S+))?(?: with cmdctl port (\d+))?')
  272. def config_remove_command(step, name, value, cmdctl_port):
  273. """
  274. Run bindctl, remove the given configuration item, and commit it.
  275. Parameters:
  276. name ('configuration <name>'): Identifier of the configuration to remove
  277. value ('value <value>'): if name is a named set, use value to identify
  278. item to remove
  279. cmdctl_port ('with cmdctl port <portnr>', optional): cmdctl port to send
  280. the command to. Defaults to 47805.
  281. Fails if cmdctl does not exit with status code 0.
  282. """
  283. cmd = "config remove " + name
  284. if value is not None:
  285. cmd = cmd + " " + value
  286. commands = [cmd,
  287. "config commit",
  288. "quit"]
  289. run_bindctl(commands, cmdctl_port)
  290. @step('send bind10(?: with cmdctl port (\d+))?( ignoring failure)? the command (.+)')
  291. def send_command(step, cmdctl_port, ignore_failure, command):
  292. """
  293. Run bindctl, send the given command, and exit bindctl.
  294. Parameters:
  295. command ('the command <command>'): The command to send.
  296. cmdctl_port ('with cmdctl port <portnr>', optional): cmdctl port to send
  297. the command to. Defaults to 47805.
  298. ignore_failure ('ignoring failure', optional): set to not None if bindctl
  299. is expected to fail (and it's acceptable).
  300. Fails if bindctl does not exit with status code 0 and ignore_failure
  301. is not None.
  302. """
  303. commands = [command,
  304. "quit"]
  305. run_bindctl(commands, cmdctl_port, ignore_failure is not None)
  306. @step('bind10 module (\S+) should( not)? be running')
  307. def module_is_running(step, name, not_str):
  308. """
  309. Convenience step to check if a module is running; can only work with
  310. default cmdctl port; sends a 'help' command with bindctl, then
  311. checks if the output contains the given name.
  312. Parameters:
  313. name ('module <name>'): The name of the module (case sensitive!)
  314. not ('not'): Reverse the check (fail if it is running)
  315. """
  316. if not_str is None:
  317. not_str = ""
  318. step.given('send bind10 the command help')
  319. step.given('last bindctl output should' + not_str + ' contain ' + name + ' exactly')
  320. @step('Configure BIND10 to run DDNS')
  321. def configure_ddns_on(step):
  322. """
  323. Convenience compound step to enable the b10-ddns module.
  324. """
  325. step.behave_as("""
  326. When I send bind10 the following commands
  327. \"\"\"
  328. config add Init/components b10-ddns
  329. config set Init/components/b10-ddns/kind dispensable
  330. config set Init/components/b10-ddns/address DDNS
  331. config commit
  332. \"\"\"
  333. """)
  334. @step('Configure BIND10 to stop running DDNS')
  335. def configure_ddns_off(step):
  336. """
  337. Convenience compound step to disable the b10-ddns module.
  338. """
  339. step.behave_as("""
  340. When I send bind10 the following commands
  341. \"\"\"
  342. config remove Init/components b10-ddns
  343. config commit
  344. \"\"\"
  345. """)
  346. @step('query statistics(?: (\S+))? of bind10 module (\S+)(?: with cmdctl port (\d+))?')
  347. def query_statistics(step, statistics, name, cmdctl_port):
  348. """
  349. query statistics data via bindctl.
  350. Parameters:
  351. statistics ('statistics <statistics>', optional) : The queried statistics name.
  352. name ('module <name>'): The name of the module (case sensitive!)
  353. cmdctl_port ('with cmdctl port <portnr>', optional): cmdctl port to send
  354. the command to.
  355. """
  356. port_str = ' with cmdctl port %s' % cmdctl_port \
  357. if cmdctl_port else ''
  358. step.given('send bind10%s the command Stats show owner=%s%s'\
  359. % (port_str, name,\
  360. ' name=%s' % statistics if statistics else ''))
  361. def find_value(dictionary, key):
  362. """A helper method. Recursively find a value corresponding to the
  363. key of the dictionary and returns it. Returns None if the
  364. dictionary is not dict type."""
  365. if type(dictionary) is not dict:
  366. return
  367. if key in dictionary:
  368. return dictionary[key]
  369. else:
  370. for v in dictionary.values():
  371. return find_value(v, key)
  372. @step('the statistics counter (\S+)(?: in the category (\S+))?'+ \
  373. '(?: for the zone (\S+))? should be' + \
  374. '(?:( greater than| less than| between))? (\-?\d+)(?: and (\-?\d+))?')
  375. def check_statistics(step, counter, category, zone, gtltbt, number, upper):
  376. """
  377. check the output of bindctl for statistics of specified counter
  378. and zone.
  379. Parameters:
  380. counter ('counter <counter>'): The counter name of statistics.
  381. category ('category <category>', optional): The category of counter.
  382. zone ('zone <zone>', optional): The zone name.
  383. gtltbt (' greater than'|' less than'|' between', optional): greater than
  384. <number> or less than <number> or between <number> and <upper>.
  385. number ('<number>): The expect counter number. <number> is assumed
  386. to be an unsigned integer.
  387. upper ('<upper>, optional): The expect upper counter number when
  388. using 'between'.
  389. """
  390. output = parse_bindctl_output_as_data_structure()
  391. found = None
  392. category_str = ""
  393. zone_str = ""
  394. depth = []
  395. if category:
  396. depth.insert(0, category)
  397. category_str = " for category %s" % category
  398. if zone:
  399. depth.insert(0, zone)
  400. zone_str = " for zone %s" % zone
  401. for level in depth:
  402. output = find_value(output, level)
  403. found = find_value(output, counter)
  404. assert found is not None, \
  405. 'Not found statistics counter %s%s%s' % \
  406. (counter, category_str, zone_str)
  407. msg = "Got %s, expected%s %s as counter %s%s" % \
  408. (found, gtltbt, number, counter, zone_str)
  409. if gtltbt and 'between' in gtltbt and upper:
  410. msg = "Got %s, expected%s %s and %s as counter %s%s" % \
  411. (found, gtltbt, number, upper, counter, zone_str)
  412. assert int(number) <= int(found) \
  413. and int(found) <= int(upper), msg
  414. elif gtltbt and 'greater' in gtltbt:
  415. assert int(found) > int(number), msg
  416. elif gtltbt and 'less' in gtltbt:
  417. assert int(found) < int(number), msg
  418. else:
  419. assert int(found) == int(number), msg
  420. @step('statistics counters are 0 in category (\S+)( except for the' + \
  421. ' following items)?')
  422. def check_statistics_items(step, category, has_except_for):
  423. """
  424. check the output of bindctl for statistics of specified counter.
  425. Parameters:
  426. category ('category <category>'): The category of counter.
  427. has_except_for ('except for the following items'): checks values of items
  428. with the multiline part.
  429. Expected values of items are taken from the multiline part of the step in
  430. the scenario. The multiline part has two columns: item_name and item_value.
  431. item_name is a relative name to category. item_value is an expected value
  432. for item_name.
  433. """
  434. def flatten(dictionary, prefix=''):
  435. h = {}
  436. for k, v in dictionary.items():
  437. if type(v) is dict:
  438. h.update(flatten(v, prefix+'.'+k))
  439. else:
  440. h[prefix+'.'+k] = v
  441. return h
  442. stats = flatten(parse_bindctl_output_as_data_structure())
  443. if has_except_for:
  444. # fetch step tables in the scnario as hashes
  445. for item in step.hashes:
  446. name = category+'.'+item['item_name']
  447. value = item['item_value']
  448. assert stats.has_key(name), \
  449. 'Statistics item %s was not found' % (name)
  450. found = stats[name]
  451. assert int(found) == int(value), \
  452. 'Statistics item %s has unexpected value %s (expect %s)' % \
  453. (name, found, value)
  454. del(stats[name])
  455. for name, found in stats.items():
  456. assert int(found) == 0, \
  457. 'Statistics item %s has unexpected value %s (expect %s)' % \
  458. (name, found, 0)