Commit e5fb3bc1 authored by Jelte Jansen's avatar Jelte Jansen
Browse files

Merge branch 'trac384'

parents 1c3d1954 c78659bd
......@@ -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,7 +91,10 @@ 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"))
......@@ -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 specifiec 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)
param = ParamInfo(name = "argument", type = "string", optional=True, desc = "If you specify the argument 'all' (before the identifier), recursively shows 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)
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)
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")
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)
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,6 +41,56 @@ 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 inthe 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:
result.append(_remove_unquoted_whitespace(text[cur_start_list_pos:pos + 1]))
start_pos = pos + 1
if i == '{' and list_count == 0:
if map_count == 0:
result.append(text[start_pos:pos + 1])
cur_start_map_pos = pos + 1
map_count = map_count + 1
elif i == '}' and list_count == 0:
if map_count > 0:
map_count = map_count - 1
if map_count == 0:
result.append(_remove_unquoted_whitespace(text[cur_start_map_pos:pos + 1]))
start_pos = pos + 1
pos = pos + 1
if start_pos <= len(text):
result.append(text[start_pos:len(text)])
return "".join(result)
class BindCmdParse:
""" This class will parse the command line usr input into three part
module name, command, parameters
......@@ -86,9 +137,12 @@ class BindCmdParse:
self._parse_params(param_str)
def _remove_list_whitespace(self, text):
return ""
def _parse_params(self, param_text):
"""convert a=b,c=d into one hash """
param_text = _remove_list_and_map_whitespace(param_text)
# Check parameter name "help"
param = NAME_PATTERN.match(param_text)
......
......@@ -16,6 +16,8 @@
"""This module holds classes representing modules, commands and
parameters for use in bindctl"""
import textwrap
try:
from collections import OrderedDict
except ImportError:
......@@ -30,6 +32,9 @@ MODULE_NODE_NAME = 'module'
COMMAND_NODE_NAME = 'command'
PARAM_NODE_NAME = 'param'
# this is used to align the descriptions in help output
CONST_BINDCTL_HELP_INDENT_WIDTH=12
class ParamInfo:
"""One parameter of one command.
......@@ -52,6 +57,12 @@ class ParamInfo:
def __str__(self):
return str("\t%s <type: %s> \t(%s)" % (self.name, self.type, self.desc))
def get_name(self):
return "%s <type: %s>" % (self.name, self.type)
def get_desc(self):
return self.desc
class CommandInfo:
"""One command which is provided by one bind10 module, it has zero
or more parameters
......@@ -68,8 +79,13 @@ class CommandInfo:
def __str__(self):
return str("%s \t(%s)" % (self.name, self.desc))
def get_name(self):
return self.name
def get_desc(self):
return self.desc;
def add_param(self, paraminfo):
"""Add a ParamInfo object to this CommandInfo"""
self.params[paraminfo.name] = paraminfo
......@@ -144,22 +160,30 @@ class CommandInfo:
del params["help"]
if len(params) == 0:
print("\tNo parameters for the command")
print("No parameters for the command")
return
print("\n\tMandatory parameters:")
print("\nMandatory parameters:")