Commit 06aeefc4 authored by Jelte's avatar Jelte
Browse files

Merge branch 'trac926'

parents c18502d5 ced9ddec
......@@ -398,6 +398,8 @@ class BindCmdInterpreter(Cmd):
print("Error: " + str(dte))
except isc.cc.data.DataNotFoundError as dnfe:
print("Error: " + str(dnfe))
except isc.cc.data.DataAlreadyPresentError as dape:
print("Error: " + str(dape))
except KeyError as ke:
print("Error: missing " + str(ke))
else:
......@@ -634,7 +636,15 @@ class BindCmdInterpreter(Cmd):
# we have more data to show
line += "/"
else:
line += "\t" + json.dumps(value_map['value'])
# if type is named_set, don't print value if None
# (it is either {} meaning empty, or None, meaning
# there actually is data, but not to be shown with
# the current command
if value_map['type'] == 'named_set' and\
value_map['value'] is None:
line += "/\t"
else:
line += "\t" + json.dumps(value_map['value'])
line += "\t" + value_map['type']
line += "\t"
if value_map['default']:
......@@ -649,10 +659,9 @@ class BindCmdInterpreter(Cmd):
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)
self.config_data.add_value(identifier,
cmd.params.get('value_or_name'),
cmd.params.get('value_for_set'))
elif cmd.command == "remove":
if 'value' in cmd.params:
self.config_data.remove_value(identifier, cmd.params['value'])
......@@ -679,7 +688,7 @@ class BindCmdInterpreter(Cmd):
except isc.config.ModuleCCSessionError as mcse:
print(str(mcse))
elif cmd.command == "diff":
print(self.config_data.get_local_changes());
print(self.config_data.get_local_changes())
elif cmd.command == "go":
self.go(identifier)
......
......@@ -50,17 +50,28 @@ def prepare_config_commands(tool):
cmd.add_param(param)
module.add_command(cmd)
cmd = CommandInfo(name = "add", desc = "Add an entry to configuration list. If no value is given, a default value is added.")
cmd = CommandInfo(name = "add", desc =
"Add an entry to configuration list or a named set. "
"When adding to a list, the command has one optional argument, "
"a value to add to the list. The value must be in correct JSON "
"and complete. When adding to a named set, it has one "
"mandatory parameter (the name to add), and an optional "
"parameter value, similar to when adding to a list. "
"In either case, when no value is given, an entry will be "
"constructed with default values.")
param = ParamInfo(name = "identifier", type = "string", optional=True, desc = DEFAULT_IDENTIFIER_DESC)
cmd.add_param(param)
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.")
param = ParamInfo(name = "value_or_name", type = "string", optional=True, desc = "Specifies a value to add to the list, or the name when adding to a named set. It must be in correct JSON format and complete.")
cmd.add_param(param)
module.add_command(cmd)
param = ParamInfo(name = "value_for_set", type = "string", optional=True, desc = "Specifies an optional value to add to the named map. 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.")
cmd = CommandInfo(name = "remove", desc = "Remove entry from configuration list or named set.")
param = ParamInfo(name = "identifier", type = "string", optional=True, desc = DEFAULT_IDENTIFIER_DESC)
cmd.add_param(param)
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.")
param = ParamInfo(name = "value", type = "string", optional=True, desc = "When identifier is a list, specifies a value to remove from the list. It must be in correct JSON format and complete. When it is a named set, specifies the name to remove.")
cmd.add_param(param)
module.add_command(cmd)
......
......@@ -511,6 +511,8 @@ Element::nameToType(const std::string& type_name) {
return (Element::list);
} else if (type_name == "map") {
return (Element::map);
} else if (type_name == "named_set") {
return (Element::map);
} else if (type_name == "null") {
return (Element::null);
} else if (type_name == "any") {
......
......@@ -67,10 +67,13 @@ check_config_item(ConstElementPtr spec) {
check_leaf_item(spec, "list_item_spec", Element::map, true);
check_config_item(spec->get("list_item_spec"));
}
// todo: add stuff for type map
if (Element::nameToType(spec->get("item_type")->stringValue()) == Element::map) {
if (spec->get("item_type")->stringValue() == "map") {
check_leaf_item(spec, "map_item_spec", Element::list, true);
check_config_item_list(spec->get("map_item_spec"));
} else if (spec->get("item_type")->stringValue() == "named_set") {
check_leaf_item(spec, "named_set_item_spec", Element::map, true);
check_config_item(spec->get("named_set_item_spec"));
}
}
......@@ -286,7 +289,8 @@ check_type(ConstElementPtr spec, ConstElementPtr element) {
return (cur_item_type == "list");
break;
case Element::map:
return (cur_item_type == "map");
return (cur_item_type == "map" ||
cur_item_type == "named_set");
break;
}
return (false);
......@@ -323,8 +327,20 @@ ModuleSpec::validateItem(ConstElementPtr spec, ConstElementPtr data,
}
}
if (data->getType() == Element::map) {
if (!validateSpecList(spec->get("map_item_spec"), data, full, errors)) {
return (false);
// either a normal 'map' or a 'named set' (determined by which
// subspecification it has)
if (spec->contains("map_item_spec")) {
if (!validateSpecList(spec->get("map_item_spec"), data, full, errors)) {
return (false);
}
} else {
typedef std::pair<std::string, ConstElementPtr> maptype;
BOOST_FOREACH(maptype m, data->mapValue()) {
if (!validateItem(spec->get("named_set_item_spec"), m.second, full, errors)) {
return (false);
}
}
}
}
return (true);
......
......@@ -211,3 +211,12 @@ TEST(ModuleSpec, CommandValidation) {
EXPECT_EQ(errors->get(0)->stringValue(), "Type mismatch");
}
TEST(ModuleSpec, NamedSetValidation) {
ModuleSpec dd = moduleSpecFromFile(specfile("spec32.spec"));
ElementPtr errors = Element::createList();
EXPECT_TRUE(dataTestWithErrors(dd, "data32_1.data", errors));
EXPECT_FALSE(dataTest(dd, "data32_2.data"));
EXPECT_FALSE(dataTest(dd, "data32_3.data"));
}
......@@ -22,6 +22,9 @@ EXTRA_DIST += data22_7.data
EXTRA_DIST += data22_8.data
EXTRA_DIST += data22_9.data
EXTRA_DIST += data22_10.data
EXTRA_DIST += data32_1.data
EXTRA_DIST += data32_2.data
EXTRA_DIST += data32_3.data
EXTRA_DIST += spec1.spec
EXTRA_DIST += spec2.spec
EXTRA_DIST += spec3.spec
......@@ -53,3 +56,4 @@ EXTRA_DIST += spec28.spec
EXTRA_DIST += spec29.spec
EXTRA_DIST += spec30.spec
EXTRA_DIST += spec31.spec
EXTRA_DIST += spec32.spec
{
"named_set_item": { "foo": 1, "bar": 2 }
}
{
"named_set_item": { "foo": "wrongtype", "bar": 2 }
}
{
"module_spec": {
"module_name": "Spec32",
"config_data": [
{ "item_name": "named_set_item",
"item_type": "named_set",
"item_optional": false,
"item_default": { "a": 1, "b": 2 },
"named_set_item_spec": {
"item_name": "named_set_element",
"item_type": "integer",
"item_optional": false,
"item_default": 3
}
}
]
}
}
......@@ -22,8 +22,22 @@
import json
class DataNotFoundError(Exception): pass
class DataTypeError(Exception): pass
class DataNotFoundError(Exception):
"""Raised if an identifier does not exist according to a spec file,
or if an item is addressed that is not in the current (or default)
config (such as a nonexistent list or map element)"""
pass
class DataAlreadyPresentError(Exception):
"""Raised if there is an attemt to add an element to a list or a
map that is already present in that list or map (i.e. if 'add'
is used when it should be 'set')"""
pass
class DataTypeError(Exception):
"""Raised if there is an attempt to set an element that is of a
different type than the type specified in the specification."""
pass
def remove_identical(a, b):
"""Removes the values from dict a that are the same as in dict b.
......
......@@ -312,7 +312,7 @@ class ModuleCCSession(ConfigData):
module_spec = isc.config.module_spec_from_file(spec_file_name)
module_cfg = ConfigData(module_spec)
module_name = module_spec.get_module_name()
self._session.group_subscribe(module_name);
self._session.group_subscribe(module_name)
# Get the current config for that module now
seq = self._session.group_sendmsg(create_command(COMMAND_GET_CONFIG, { "module_name": module_name }), "ConfigManager")
......@@ -327,7 +327,7 @@ class ModuleCCSession(ConfigData):
rcode, value = parse_answer(answer)
if rcode == 0:
if value != None and module_spec.validate_config(False, value):
module_cfg.set_local_config(value);
module_cfg.set_local_config(value)
if config_update_callback is not None:
config_update_callback(value, module_cfg)
......@@ -377,7 +377,7 @@ class ModuleCCSession(ConfigData):
if self.get_module_spec().validate_config(False,
value,
errors):
self.set_local_config(value);
self.set_local_config(value)
if self._config_handler:
self._config_handler(value)
else:
......@@ -414,8 +414,8 @@ class UIModuleCCSession(MultiConfigData):
self.set_specification(isc.config.ModuleSpec(specs[module]))
def update_specs_and_config(self):
self.request_specifications();
self.request_current_config();
self.request_specifications()
self.request_current_config()
def request_current_config(self):
"""Requests the current configuration from the configuration
......@@ -425,47 +425,90 @@ class UIModuleCCSession(MultiConfigData):
raise ModuleCCSessionError("Bad config version")
self._set_current_config(config)
def add_value(self, identifier, value_str = None):
"""Add a value to a configuration list. Raises a DataTypeError
if the value does not conform to the list_item_spec field
of the module config data specification. If value_str is
not given, we add the default as specified by the .spec
file."""
module_spec = self.find_spec_part(identifier)
if (type(module_spec) != dict or "list_item_spec" not in module_spec):
raise isc.cc.data.DataNotFoundError(str(identifier) + " is not a list")
def _add_value_to_list(self, identifier, value):
cur_list, status = self.get_value(identifier)
if not cur_list:
cur_list = []
# Hmm. Do we need to check for duplicates?
value = None
if value_str is not None:
value = isc.cc.data.parse_value_str(value_str)
else:
if value is None:
if "item_default" in module_spec["list_item_spec"]:
value = module_spec["list_item_spec"]["item_default"]
if value is None:
raise isc.cc.data.DataNotFoundError("No value given and no default for " + str(identifier))
raise isc.cc.data.DataNotFoundError(
"No value given and no default for " + str(identifier))
if value not in cur_list:
cur_list.append(value)
self.set_value(identifier, cur_list)
else:
raise isc.cc.data.DataAlreadyPresentError(value +
" already in "
+ identifier)
def _add_value_to_named_set(self, identifier, value, item_value):
if type(value) != str:
raise isc.cc.data.DataTypeError("Name for named_set " +
identifier +
" must be a string")
# fail on both None and empty string
if not value:
raise isc.cc.data.DataNotFoundError(
"Need a name to add a new item to named_set " +
str(identifier))
else:
cur_map, status = self.get_value(identifier)
if not cur_map:
cur_map = {}
if value not in cur_map:
cur_map[value] = item_value
self.set_value(identifier, cur_map)
else:
raise isc.cc.data.DataAlreadyPresentError(value +
" already in "
+ identifier)
def remove_value(self, identifier, value_str):
"""Remove a value from a configuration list. The value string
must be a string representation of the full item. Raises
a DataTypeError if the value at the identifier is not a list,
or if the given value_str does not match the list_item_spec
"""
def add_value(self, identifier, value_str = None, set_value_str = None):
"""Add a value to a configuration list. Raises a DataTypeError
if the value does not conform to the list_item_spec field
of the module config data specification. If value_str is
not given, we add the default as specified by the .spec
file. Raises a DataNotFoundError if the given identifier
is not specified in the specification as a map or list.
Raises a DataAlreadyPresentError if the specified element
already exists."""
module_spec = self.find_spec_part(identifier)
if (type(module_spec) != dict or "list_item_spec" not in module_spec):
raise isc.cc.data.DataNotFoundError(str(identifier) + " is not a list")
if module_spec is None:
raise isc.cc.data.DataNotFoundError("Unknown item " + str(identifier))
# the specified element must be a list or a named_set
if 'list_item_spec' in module_spec:
value = None
# in lists, we might get the value with spaces, making it
# the third argument. In that case we interpret both as
# one big string meant as the value
if value_str is not None:
if set_value_str is not None:
value_str += set_value_str
value = isc.cc.data.parse_value_str(value_str)
self._add_value_to_list(identifier, value)
elif 'named_set_item_spec' in module_spec:
item_name = None
item_value = None
if value_str is not None:
item_name = isc.cc.data.parse_value_str(value_str)
if set_value_str is not None:
item_value = isc.cc.data.parse_value_str(set_value_str)
else:
if 'item_default' in module_spec['named_set_item_spec']:
item_value = module_spec['named_set_item_spec']['item_default']
self._add_value_to_named_set(identifier, item_name,
item_value)
else:
raise isc.cc.data.DataNotFoundError(str(identifier) + " is not a list or a named set")
if value_str is None:
def _remove_value_from_list(self, identifier, value):
if value is None:
# we are directly removing an list index
id, list_indices = isc.cc.data.split_identifier_list_indices(identifier)
if list_indices is None:
......@@ -473,17 +516,52 @@ class UIModuleCCSession(MultiConfigData):
else:
self.set_value(identifier, None)
else:
value = isc.cc.data.parse_value_str(value_str)
isc.config.config_data.check_type(module_spec, [value])
cur_list, status = self.get_value(identifier)
#if not cur_list:
# cur_list = isc.cc.data.find_no_exc(self.config.data, identifier)
if not cur_list:
cur_list = []
if value in cur_list:
elif value in cur_list:
cur_list.remove(value)
self.set_value(identifier, cur_list)
def _remove_value_from_named_set(self, identifier, value):
if value is None:
raise isc.cc.data.DataNotFoundError("Need a name to remove an item from named_set " + str(identifier))
elif type(value) != str:
raise isc.cc.data.DataTypeError("Name for named_set " + identifier + " must be a string")
else:
cur_map, status = self.get_value(identifier)
if not cur_map:
cur_map = {}
if value in cur_map:
del cur_map[value]
else:
raise isc.cc.data.DataNotFoundError(value + " not found in named_set " + str(identifier))
def remove_value(self, identifier, value_str):
"""Remove a value from a configuration list or named set.
The value string must be a string representation of the full
item. Raises a DataTypeError if the value at the identifier
is not a list, or if the given value_str does not match the
list_item_spec """
module_spec = self.find_spec_part(identifier)
if module_spec is None:
raise isc.cc.data.DataNotFoundError("Unknown item " + str(identifier))
value = None
if value_str is not None:
value = isc.cc.data.parse_value_str(value_str)
if 'list_item_spec' in module_spec:
if value is not None:
isc.config.config_data.check_type(module_spec['list_item_spec'], value)
self._remove_value_from_list(identifier, value)
elif 'named_set_item_spec' in module_spec:
self._remove_value_from_named_set(identifier, value)
else:
raise isc.cc.data.DataNotFoundError(str(identifier) + " is not a list or a named_set")
def commit(self):
"""Commit all local changes, send them through b10-cmdctl to
the configuration manager"""
......
......@@ -145,6 +145,8 @@ def _find_spec_part_single(cur_spec, id_part):
return cur_spec['list_item_spec']
# not found
raise isc.cc.data.DataNotFoundError(id + " not found")
elif type(cur_spec) == dict and 'named_set_item_spec' in cur_spec.keys():
return cur_spec['named_set_item_spec']
elif type(cur_spec) == list:
for cur_spec_item in cur_spec:
if cur_spec_item['item_name'] == id:
......@@ -191,11 +193,14 @@ def spec_name_list(spec, prefix="", recurse=False):
result.extend(spec_name_list(map_el['map_item_spec'], prefix + map_el['item_name'], recurse))
else:
result.append(prefix + name)
elif 'named_set_item_spec' in spec:
# we added a '/' above, but in this one case we don't want it
result.append(prefix[:-1])
else:
for name in spec:
result.append(prefix + name + "/")
if recurse:
result.extend(spec_name_list(spec[name],name, recurse))
result.extend(spec_name_list(spec[name], name, recurse))
elif type(spec) == list:
for list_el in spec:
if 'item_name' in list_el:
......@@ -207,7 +212,7 @@ def spec_name_list(spec, prefix="", recurse=False):
else:
raise ConfigDataError("Bad specification")
else:
raise ConfigDataError("Bad specication")
raise ConfigDataError("Bad specification")
return result
class ConfigData:
......@@ -255,7 +260,7 @@ class ConfigData:
def get_local_config(self):
"""Returns the non-default config values in a dict"""
return self.data;
return self.data
def get_item_list(self, identifier = None, recurse = False):
"""Returns a list of strings containing the full identifiers of
......@@ -412,7 +417,39 @@ class MultiConfigData:
item_id, list_indices = isc.cc.data.split_identifier_list_indices(id_part)
id_list = module + "/" + id_prefix + "/" + item_id
id_prefix += "/" + id_part
if list_indices is not None:
part_spec = find_spec_part(self._specifications[module].get_config_spec(), id_prefix)
if part_spec['item_type'] == 'named_set':
# For named sets, the identifier is partly defined
# by which values are actually present, and not
# purely by the specification.
# So if there is a part of the identifier left,
# we need to look up the value, then see if that
# contains the next part of the identifier we got
if len(id_parts) == 0:
if 'item_default' in part_spec:
return part_spec['item_default']
else:
return None
id_part = id_parts.pop(0)
named_set_value, type = self.get_value(id_list)
if id_part in named_set_value:
if len(id_parts) > 0:
# we are looking for the *default* value.
# so if not present in here, we need to
# lookup the one from the spec
rest_of_id = "/".join(id_parts)
result = isc.cc.data.find_no_exc(named_set_value[id_part], rest_of_id)
if result is None:
spec_part = self.find_spec_part(identifier)
if 'item_default' in spec_part:
return spec_part['item_default']
return result
else:
return named_set_value[id_part]
else:
return None
elif list_indices is not None:
# there's actually two kinds of default here for
# lists; they can have a default value (like an
# empty list), but their elements can also have
......@@ -449,7 +486,12 @@ class MultiConfigData:
spec = find_spec_part(self._specifications[module].get_config_spec(), id)
if 'item_default' in spec:
return spec['item_default']
# one special case, named_set
if spec['item_type'] == 'named_set':
print("is " + id_part + " in named set?")
return spec['item_default']
else:
return spec['item_default']
else:
return None
......@@ -493,7 +535,7 @@ class MultiConfigData:
spec_part_list = spec_part['list_item_spec']
list_value, status = self.get_value(identifier)
if list_value is None:
raise isc.cc.data.DataNotFoundError(identifier)
raise isc.cc.data.DataNotFoundError(identifier + " not found")
if type(list_value) != list:
# the identifier specified a single element
......@@ -509,12 +551,38 @@ class MultiConfigData:
for i in range(len(list_value)):
self._append_value_item(result, spec_part_list, "%s[%d]" % (identifier, i), all)
elif item_type == "map":
value, status = self.get_value(identifier)
# just show the specific contents of a map, we are
# almost never interested in just its name
spec_part_map = spec_part['map_item_spec']
self._append_value_item(result, spec_part_map, identifier, all)
elif item_type == "named_set":
value, status = self.get_value(identifier)
# show just the one entry, when either the map is empty,
# or when this is element is not requested specifically
if len(value.keys()) == 0:
entry = _create_value_map_entry(identifier,
item_type,
{}, status)
result.append(entry)
elif not first and not all:
entry = _create_value_map_entry(identifier,
item_type,
None, status)
result.append(entry)
else:
spec_part_named_set = spec_part['named_set_item_spec']
for entry in value:
self._append_value_item(result,
spec_part_named_set,
identifier + "/" + entry,
all)
else:
value, status = self.get_value(identifier)
if status == self.NONE and not spec_part['item_optional']:
raise isc.cc.data.DataNotFoundError(identifier + " not found")
entry = _create_value_map_entry(identifier,
item_type,
value, status)
......@@ -569,7 +637,7 @@ class MultiConfigData:
spec_part = spec_part['list_item_spec']
check_type(spec_part, value)
else:
raise isc.cc.data.DataNotFoundError(identifier)
raise isc.cc.data.DataNotFoundError(identifier + " not found")
# Since we do not support list diffs (yet?), we need to
# copy the currently set list of items to _local_changes
......@@ -579,15 +647,50 @@ class MultiConfigData:
cur_id_part = '/'
for id_part in id_parts:
id, list_indices = isc.cc.data.split_identifier_list_indices(id_part)
cur_value, status = self.get_value(cur_id_part + id)
# Check if the value was there in the first place
if status == MultiConfigData.NONE and cur_id_part != "/":