Commit 319debfb authored by chenzhengzhang's avatar chenzhengzhang
Browse files

Merge branch 'master' into trac346

parents ed89ba2b 80a4c6e8
172. [func] jelte
Improved the bindctl cli in various ways, mainly concerning
list and map item addressing, the correct display of actual values,
and internal help.
(Trac #384, git e5fb3bc1ed5f3c0aec6eb40a16c63f3d0fc6a7b2)
171. [func] feng, jerry, jinmei, vorner
b10-auth, src/lib/datasrc: in memory data source now works as a
complete data source for authoritative DNS servers and b10-auth
uses it. It still misses major features, however, including
DNSSEC support and zone transfer.
(Last trac #552, but many more,
git 6f031a09a248e7684723c000f3e8cc981dcdb349)
170. [bug] jinmei
Tightened validity checks in the NSEC3 constructors, both "from
"text" and "from wire". Specifically, wire data containing
invalid type bitmaps or invalid lengths of salt or hash is now
correctly rejected.
(Trac #117, git 9c690982f24fef19c747a72f43c4298333a58f48)
169. [func] zhang likun, jelte
Added a basic implementation for a resolver cache (though not
used yet).
(Trac #449, git 8aa3b2246ae095bbe7f855fd11656ae3bdb98986)
168. [bug] vorner
Boss no longer has the -f argument, which was undocumented and stayed as
a relict of previous versions, currently causing only strange behaviour.
Boss no longer has the -f argument, which was undocumented and
stayed as a relict of previous versions, currently causing only
strange behaviour.
(Trac #572, git 17f237478961005707d649a661cc72a4a0d612d4)
167. [bug] naokikambe
Fixed failure of termination of msgq_test.py with python3 coverage(3.3.1)
Fixed failure of termination of msgq_test.py with python3
coverage(3.3.1)
(Trac #573, git 0e6a18e12f61cc482e07078776234f32605312e5)
166. [func] jelte
......@@ -30,10 +53,10 @@
(Trac #452, git c9f6acc81e24c4b8f0eb351123dc7b43f64e0914)
163. [func] vorner
The pimpl design pattern is used in UDPServer, with a shared pointer. This
makes it smaller to copy (which is done a lot as a sideeffect of being
coroutine) and speeds applications of this class (notably b10-auth) up by
around 10%.
The pimpl design pattern is used in UDPServer, with a shared
pointer. This makes it smaller to copy (which is done a lot as a
sideeffect of being coroutine) and speeds applications of this
class (notably b10-auth) up by around 10%.
(Trac #537, git 94cb95b1d508541201fc064302ba836164d3cbe6)
162. [func] stephen
......
......@@ -12,45 +12,44 @@
"item_type": "list",
"item_optional": true,
"item_default": [],
"list_item_spec": {
"item_name": "list_element",
"list_item_spec":
{ "item_name": "list_element",
"item_type": "map",
"item_optional": false,
"item_default": {},
"map_item_spec": [
{ "item_name": "type",
"item_type": "string",
"item_optional": false,
"item_default": ""
},
{ "item_name": "class",
"item_type": "string",
"item_optional": false,
"item_default": "IN"
},
{ "item_name": "zones",
"item_type": "list",
"item_optional": false,
"item_default": [],
"list_item_spec": {
"item_name": "list_element",
"item_type": "map",
"item_optional": true,
"map_item_spec": [
{ "item_name": "origin",
"item_type": "string",
"item_optional": false,
"item_default": ""
},
{ "item_name": "file",
"item_type": "string",
"item_optional": false,
"item_default": ""
}
]
}
}
]
"map_item_spec": [
{ "item_name": "type",
"item_type": "string",
"item_optional": false,
"item_default": ""
},
{ "item_name": "class",
"item_type": "string",
"item_optional": false,
"item_default": "IN"
},
{ "item_name": "zones",
"item_type": "list",
"item_optional": false,
"item_default": [],
"list_item_spec":
{ "item_name": "list_element",
"item_type": "map",
"item_optional": true,
"item_default": { "origin": "", "file": "" },
"map_item_spec": [
{ "item_name": "origin",
"item_type": "string",
"item_optional": false,
"item_default": ""
},
{ "item_name": "file",
"item_type": "string",
"item_optional": false,
"item_default": ""
}]
}
}]
}
},
{ "item_name": "statistics-interval",
......
......@@ -51,7 +51,6 @@ except ImportError:
my_readline = sys.stdin.readline
CSV_FILE_NAME = 'default_user.csv'
FAIL_TO_CONNECT_WITH_CMDCTL = "Fail to connect with b10-cmdctl module, is it running?"
CONFIG_MODULE_NAME = 'config'
CONST_BINDCTL_HELP = """
usage: <module name> <command name> [param1 = value1 [, param2 = value2]]
......@@ -92,10 +91,13 @@ class BindCmdInterpreter(Cmd):
Cmd.__init__(self)
self.location = ""
self.prompt_end = '> '
self.prompt = self.prompt_end
if sys.stdin.isatty():
self.prompt = self.prompt_end
else:
self.prompt = ""
self.ruler = '-'
self.modules = OrderedDict()
self.add_module_info(ModuleInfo("help", desc = "Get help for bindctl"))
self.add_module_info(ModuleInfo("help", desc = "Get help for bindctl."))
self.server_port = server_port
self.conn = ValidatedHTTPSConnection(self.server_port,
ca_certs=pem_file)
......@@ -119,8 +121,8 @@ class BindCmdInterpreter(Cmd):
self.cmdloop()
except FailToLogin as err:
print(err)
print(FAIL_TO_CONNECT_WITH_CMDCTL)
# error already printed when this was raised, ignoring
pass
except KeyboardInterrupt:
print('\nExit from bindctl')
......@@ -270,8 +272,10 @@ class BindCmdInterpreter(Cmd):
return line
def postcmd(self, stop, line):
'''Update the prompt after every command'''
self.prompt = self.location + self.prompt_end
'''Update the prompt after every command, but only if we
have a tty as output'''
if sys.stdin.isatty():
self.prompt = self.location + self.prompt_end
return stop
def _prepare_module_commands(self, module_spec):
......@@ -375,7 +379,14 @@ class BindCmdInterpreter(Cmd):
if cmd.command == "help" or ("help" in cmd.params.keys()):
self._handle_help(cmd)
elif cmd.module == CONFIG_MODULE_NAME:
self.apply_config_cmd(cmd)
try:
self.apply_config_cmd(cmd)
except isc.cc.data.DataTypeError as dte:
print("Error: " + str(dte))
except isc.cc.data.DataNotFoundError as dnfe:
print("Error: " + str(dnfe))
except KeyError as ke:
print("Error: missing " + str(ke))
else:
self.apply_cmd(cmd)
......@@ -396,9 +407,24 @@ class BindCmdInterpreter(Cmd):
def do_help(self, name):
print(CONST_BINDCTL_HELP)
for k in self.modules.keys():
print("\t", self.modules[k])
for k in self.modules.values():
n = k.get_name()
if len(n) >= CONST_BINDCTL_HELP_INDENT_WIDTH:
print(" %s" % n)
print(textwrap.fill(k.get_desc(),
initial_indent=" ",
subsequent_indent=" " +
" " * CONST_BINDCTL_HELP_INDENT_WIDTH,
width=70))
else:
print(textwrap.fill("%s%s%s" %
(k.get_name(),
" "*(CONST_BINDCTL_HELP_INDENT_WIDTH - len(k.get_name())),
k.get_desc()),
initial_indent=" ",
subsequent_indent=" " +
" " * CONST_BINDCTL_HELP_INDENT_WIDTH,
width=70))
def onecmd(self, line):
if line == 'EOF' or line.lower() == "quit":
......@@ -411,7 +437,19 @@ class BindCmdInterpreter(Cmd):
Cmd.onecmd(self, line)
def remove_prefix(self, list, prefix):
return [(val[len(prefix):]) for val in list]
"""Removes the prefix already entered, and all elements from the
list that don't match it"""
if prefix.startswith('/'):
prefix = prefix[1:]
new_list = []
for val in list:
if val.startswith(prefix):
new_val = val[len(prefix):]
if new_val.startswith("/"):
new_val = new_val[1:]
new_list.append(new_val)
return new_list
def complete(self, text, state):
if 0 == state:
......@@ -502,8 +540,7 @@ class BindCmdInterpreter(Cmd):
self._validate_cmd(cmd)
self._handle_cmd(cmd)
except (IOError, http.client.HTTPException) as err:
print('Error!', err)
print(FAIL_TO_CONNECT_WITH_CMDCTL)
print('Error: ', err)
except BindCtlException as err:
print("Error! ", err)
self._print_correct_usage(err)
......@@ -541,87 +578,115 @@ class BindCmdInterpreter(Cmd):
Raises a KeyError if the command was not complete
'''
identifier = self.location
try:
if 'identifier' in cmd.params:
if not identifier.endswith("/"):
identifier += "/"
if cmd.params['identifier'].startswith("/"):
identifier = cmd.params['identifier']
else:
identifier += cmd.params['identifier']
# Check if the module is known; for unknown modules
# we currently deny setting preferences, as we have
# no way yet to determine if they are ok.
module_name = identifier.split('/')[1]
if self.config_data is None or \
not self.config_data.have_specification(module_name):
print("Error: Module '" + module_name + "' unknown or not running")
return
if 'identifier' in cmd.params:
if not identifier.endswith("/"):
identifier += "/"
if cmd.params['identifier'].startswith("/"):
identifier = cmd.params['identifier']
else:
if cmd.params['identifier'].startswith('['):
identifier = identifier[:-1]
identifier += cmd.params['identifier']
# Check if the module is known; for unknown modules
# we currently deny setting preferences, as we have
# no way yet to determine if they are ok.
module_name = identifier.split('/')[1]
if module_name != "" and (self.config_data is None or \
not self.config_data.have_specification(module_name)):
print("Error: Module '" + module_name + "' unknown or not running")
return
if cmd.command == "show":
values = self.config_data.get_value_maps(identifier)
for value_map in values:
line = value_map['name']
if value_map['type'] in [ 'module', 'map', 'list' ]:
line += "/"
else:
line += ":\t" + json.dumps(value_map['value'])
line += "\t" + value_map['type']
line += "\t"
if value_map['default']:
line += "(default)"
if value_map['modified']:
line += "(modified)"
print(line)
elif cmd.command == "add":
self.config_data.add_value(identifier, cmd.params['value'])
elif cmd.command == "remove":
if 'value' in cmd.params:
self.config_data.remove_value(identifier, cmd.params['value'])
if cmd.command == "show":
# check if we have the 'all' argument
show_all = False
if 'argument' in cmd.params:
if cmd.params['argument'] == 'all':
show_all = True
elif 'identifier' not in cmd.params:
# no 'all', no identifier, assume this is the
#identifier
identifier += cmd.params['argument']
else:
self.config_data.remove_value(identifier, None)
elif cmd.command == "set":
if 'identifier' not in cmd.params:
print("Error: missing identifier or value")
print("Error: unknown argument " + cmd.params['argument'] + ", or multiple identifiers given")
return
values = self.config_data.get_value_maps(identifier, show_all)
for value_map in values:
line = value_map['name']
if value_map['type'] in [ 'module', 'map' ]:
line += "/"
elif value_map['type'] == 'list' \
and value_map['value'] != []:
# do not print content of non-empty lists if
# we have more data to show
line += "/"
else:
parsed_value = None
try:
parsed_value = json.loads(cmd.params['value'])
except Exception as exc:
# ok could be an unquoted string, interpret as such
parsed_value = cmd.params['value']
self.config_data.set_value(identifier, parsed_value)
elif cmd.command == "unset":
self.config_data.unset(identifier)
elif cmd.command == "revert":
self.config_data.clear_local_changes()
elif cmd.command == "commit":
self.config_data.commit()
elif cmd.command == "diff":
print(self.config_data.get_local_changes());
elif cmd.command == "go":
self.go(identifier)
except isc.cc.data.DataTypeError as dte:
print("Error: " + str(dte))
except isc.cc.data.DataNotFoundError as dnfe:
print("Error: " + identifier + " not found")
except KeyError as ke:
print("Error: missing " + str(ke))
raise ke
line += "\t" + json.dumps(value_map['value'])
line += "\t" + value_map['type']
line += "\t"
if value_map['default']:
line += "(default)"
if value_map['modified']:
line += "(modified)"
print(line)
elif cmd.command == "show_json":
if identifier == "":
print("Need at least the module to show the configuration in JSON format")
else:
data, default = self.config_data.get_value(identifier)
print(json.dumps(data))
elif cmd.command == "add":
if 'value' in cmd.params:
self.config_data.add_value(identifier, cmd.params['value'])
else:
self.config_data.add_value(identifier)
elif cmd.command == "remove":
if 'value' in cmd.params:
self.config_data.remove_value(identifier, cmd.params['value'])
else:
self.config_data.remove_value(identifier, None)
elif cmd.command == "set":
if 'identifier' not in cmd.params:
print("Error: missing identifier or value")
else:
parsed_value = None
try:
parsed_value = json.loads(cmd.params['value'])
except Exception as exc:
# ok could be an unquoted string, interpret as such
parsed_value = cmd.params['value']
self.config_data.set_value(identifier, parsed_value)
elif cmd.command == "unset":
self.config_data.unset(identifier)
elif cmd.command == "revert":
self.config_data.clear_local_changes()
elif cmd.command == "commit":
self.config_data.commit()
elif cmd.command == "diff":
print(self.config_data.get_local_changes());
elif cmd.command == "go":
self.go(identifier)
def go(self, identifier):
'''Handles the config go command, change the 'current' location
within the configuration tree'''
# this is just to see if it exists
self.config_data.get_value(identifier)
# some sanitizing
identifier = identifier.replace("//", "/")
if not identifier.startswith("/"):
identifier = "/" + identifier
if identifier.endswith("/"):
identifier = identifier[:-1]
self.location = identifier
within the configuration tree. '..' will be interpreted as
'up one level'.'''
id_parts = isc.cc.data.split_identifier(identifier)
new_location = ""
for id_part in id_parts:
if (id_part == ".."):
# go 'up' one level
new_location, a, b = new_location.rpartition("/")
else:
new_location += "/" + id_part
# check if exists, if not, revert and error
v,d = self.config_data.get_value(new_location)
if v is None:
print("Error: " + identifier + " not found")
return
self.location = new_location
def apply_cmd(self, cmd):
'''Handles a general module command'''
......
......@@ -33,51 +33,60 @@ isc.util.process.rename()
# number, and the overall BIND 10 version number (set in configure.ac).
VERSION = "bindctl 20101201 (BIND 10 @PACKAGE_VERSION@)"
DEFAULT_IDENTIFIER_DESC = "The identifier specifies the config item. Child elements are separated with the '/' character. List indices can be specified with '[i]', where i is an integer specifying the index, starting with 0. Examples: 'Boss/start_auth', 'Recurse/listen_on[0]/address'. If no identifier is given, shows the item at the current location."
def prepare_config_commands(tool):
'''Prepare fixed commands for local configuration editing'''
module = ModuleInfo(name = CONFIG_MODULE_NAME, desc = "Configuration commands")
cmd = CommandInfo(name = "show", desc = "Show configuration")
param = ParamInfo(name = "identifier", type = "string", optional=True)
module = ModuleInfo(name = CONFIG_MODULE_NAME, desc = "Configuration commands.")
cmd = CommandInfo(name = "show", desc = "Show configuration.")
param = ParamInfo(name = "argument", type = "string", optional=True, desc = "If you specify the argument 'all' (before the identifier), recursively show all child elements for the given identifier.")
cmd.add_param(param)
param = ParamInfo(name = "identifier", type = "string", optional=True, desc = DEFAULT_IDENTIFIER_DESC)
cmd.add_param(param)
module.add_command(cmd)
cmd = CommandInfo(name = "show_json", desc = "Show full configuration in JSON format.")
param = ParamInfo(name = "identifier", type = "string", optional=True, desc = DEFAULT_IDENTIFIER_DESC)
cmd.add_param(param)
module.add_command(cmd)
cmd = CommandInfo(name = "add", desc = "Add entry to configuration list")
param = ParamInfo(name = "identifier", type = "string", optional=True)
cmd = CommandInfo(name = "add", desc = "Add an entry to configuration list. If no value is given, a default value is added.")
param = ParamInfo(name = "identifier", type = "string", optional=True, desc = DEFAULT_IDENTIFIER_DESC)
cmd.add_param(param)
param = ParamInfo(name = "value", type = "string", optional=False)
param = ParamInfo(name = "value", type = "string", optional=True, desc = "Specifies a value to add to the list. It must be in correct JSON format and complete.")
cmd.add_param(param)
module.add_command(cmd)
cmd = CommandInfo(name = "remove", desc = "Remove entry from configuration list")
param = ParamInfo(name = "identifier", type = "string", optional=True)
cmd = CommandInfo(name = "remove", desc = "Remove entry from configuration list.")
param = ParamInfo(name = "identifier", type = "string", optional=True, desc = DEFAULT_IDENTIFIER_DESC)
cmd.add_param(param)
param = ParamInfo(name = "value", type = "string", optional=True)
param = ParamInfo(name = "value", type = "string", optional=True, desc = "Specifies a value to remove from the list. It must be in correct JSON format and complete.")
cmd.add_param(param)
module.add_command(cmd)
cmd = CommandInfo(name = "set", desc = "Set a configuration value")
param = ParamInfo(name = "identifier", type = "string", optional=True)
cmd = CommandInfo(name = "set", desc = "Set a configuration value.")
param = ParamInfo(name = "identifier", type = "string", optional=True, desc = DEFAULT_IDENTIFIER_DESC)
cmd.add_param(param)
param = ParamInfo(name = "value", type = "string", optional=False)
param = ParamInfo(name = "value", type = "string", optional=False, desc = "Specifies a value to set. It must be in correct JSON format and complete.")
cmd.add_param(param)
module.add_command(cmd)
cmd = CommandInfo(name = "unset", desc = "Unset a configuration value")
param = ParamInfo(name = "identifier", type = "string", optional=False)
cmd = CommandInfo(name = "unset", desc = "Unset a configuration value (i.e. revert to the default, if any).")
param = ParamInfo(name = "identifier", type = "string", optional=False, desc = DEFAULT_IDENTIFIER_DESC)
cmd.add_param(param)
module.add_command(cmd)
cmd = CommandInfo(name = "diff", desc = "Show all local changes")
cmd = CommandInfo(name = "diff", desc = "Show all local changes that have not been committed.")
module.add_command(cmd)
cmd = CommandInfo(name = "revert", desc = "Revert all local changes")
cmd = CommandInfo(name = "revert", desc = "Revert all local changes.")
module.add_command(cmd)
cmd = CommandInfo(name = "commit", desc = "Commit all local changes")
cmd = CommandInfo(name = "commit", desc = "Commit all local changes.")
module.add_command(cmd)
cmd = CommandInfo(name = "go", desc = "Go to a specific configuration part")
param = ParamInfo(name = "identifier", type="string", optional=False)
cmd = CommandInfo(name = "go", desc = "Go to a specific configuration part.")
param = ParamInfo(name = "identifier", type="string", optional=False, desc = DEFAULT_IDENTIFIER_DESC)
cmd.add_param(param)
module.add_command(cmd)
......@@ -115,15 +124,12 @@ def set_bindctl_options(parser):
help = 'PEM formatted server certificate validation chain file')
if __name__ == '__main__':
try:
parser = OptionParser(version = VERSION)
set_bindctl_options(parser)
(options, args) = parser.parse_args()
server_addr = options.addr + ':' + str(options.port)
tool = BindCmdInterpreter(server_addr, pem_file=options.cert_chain)
prepare_config_commands(tool)
tool.run()
except Exception as e:
print(e, "\nFailed to connect with b10-cmdctl module, is it running?")
parser = OptionParser(version = VERSION)
set_bindctl_options(parser)
(options, args) = parser.parse_args()
server_addr = options.addr + ':' + str(options.port)
tool = BindCmdInterpreter(server_addr, pem_file=options.cert_chain)
prepare_config_commands(tool)
tool.run()
......@@ -33,6 +33,7 @@ param_value_str = "(?P<param_value>[^\'\" ][^, ]+)"
param_value_with_quota_str = "[\"\'](?P<param_value>.+?)(?<!\\\)[\"\']"
next_params_str = "(?P<blank>\s*)(?P<comma>,?)(?P<next_params>.*)$"
PARAM_WITH_QUOTA_PATTERN = re.compile(param_name_str +
param_value_with_quota_str +
next_params_str)
......@@ -40,8 +41,58 @@ PARAM_PATTERN = re.compile(param_name_str + param_value_str + next_params_str)
# Used for module and command name
NAME_PATTERN = re.compile("^\s*(?P<name>[\w]+)(?P<blank>\s*)(?P<others>.*)$")
# this removes all whitespace in the given string, except when
# between " quotes
_remove_unquoted_whitespace = \
lambda text:'"'.join( it if i%2 else ''.join(it.split())
for i,it in enumerate(text.split('"')) )
def _remove_list_and_map_whitespace(text):
"""Returns a string where the whitespace between matching [ and ]
is removed, unless quoted"""
# regular expression aren't really the right tool, since we may have
# nested structures
result = []
start_pos = 0
pos = 0
list_count = 0
map_count = 0
cur_start_list_pos = None
cur_start_map_pos = None
for i in text:
if i == '[' and map_count == 0:
if list_count == 0:
result.append(text[start_pos:pos + 1])
cur_start_list_pos = pos + 1
list_count = list_count + 1
elif i == ']' and map_count == 0:
if list_count > 0:
list_count = list_count - 1
if list_count == 0: