/* * Copyright (C) 2010 Vyatta, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include // for debian's version comparison algorithm #define APT_COMPATIBILITY 986 #include #include #include #include ////// constants //// node status const string Cstore::C_NODE_STATUS_DELETED = "deleted"; const string Cstore::C_NODE_STATUS_ADDED = "added"; const string Cstore::C_NODE_STATUS_CHANGED = "changed"; const string Cstore::C_NODE_STATUS_STATIC = "static"; //// env vars for shell // current levels const string Cstore::C_ENV_EDIT_LEVEL = "VYATTA_EDIT_LEVEL"; const string Cstore::C_ENV_TMPL_LEVEL = "VYATTA_TEMPLATE_LEVEL"; // shell-specific vars const string Cstore::C_ENV_SHELL_PROMPT = "PS1"; const string Cstore::C_ENV_SHELL_CWORDS = "COMP_WORDS"; const string Cstore::C_ENV_SHELL_CWORD_COUNT = "COMP_CWORD"; // shell api vars const string Cstore::C_ENV_SHAPI_COMP_VALS = "_cli_shell_api_comp_values"; const string Cstore::C_ENV_SHAPI_LCOMP_VAL = "_cli_shell_api_last_comp_val"; const string Cstore::C_ENV_SHAPI_COMP_HELP = "_cli_shell_api_comp_help"; const string Cstore::C_ENV_SHAPI_HELP_ITEMS = "_cli_shell_api_hitems"; const string Cstore::C_ENV_SHAPI_HELP_STRS = "_cli_shell_api_hstrs"; //// dirs/files const string Cstore::C_ENUM_SCRIPT_DIR = "/opt/vyatta/share/enumeration"; const string Cstore::C_LOGFILE_STDOUT = "/tmp/cfg-stdout.log"; //// sorting const unsigned int Cstore::SORT_DEFAULT = 0; const unsigned int Cstore::SORT_DEB_VERSION = 0; const unsigned int Cstore::SORT_NONE = 1; ////// static bool Cstore::_init = false; Cstore::MapT Cstore::_sort_func_map; ////// constructors/destructors /* this constructor just returns the generic environment string, * currently the two levels. implementation-specific environment * (e.g., unionfs stuff) is handled by derived class. * * note: currently using original semantics for the levels, i.e., they * represent the actual physical paths, which involve fs-specific * escaping. this should be changed to a "logical" representation * so that their manipulation can be moved from derived class to * this base class. */ Cstore::Cstore(string& env) { init(); string decl = "declare -x "; env = (decl + C_ENV_EDIT_LEVEL + "=/; "); env += (decl + C_ENV_TMPL_LEVEL + "=/;"); } ////// public interface /* check if specified "logical path" corresponds to a valid template. * validate_vals: whether to validate "values" along specified path. * return true if valid. otherwise return false. */ bool Cstore::validateTmplPath(const vector& path_comps, bool validate_vals) { vtw_def def; // if we can get parsed tmpl, path is valid return get_parsed_tmpl(path_comps, validate_vals, def); } /* same as above but return parsed template. * def: (output) parsed template. * note: if last path component is "value" (i.e., def.is_value), parsed * template is actually at "full path - 1". see get_parsed_tmpl() for details. */ bool Cstore::validateTmplPath(const vector& path_comps, bool validate_vals, vtw_def& def) { // if we can get parsed tmpl, path is valid return get_parsed_tmpl(path_comps, validate_vals, def); } /* get parsed template of specified path as a string-string map * tmap: (output) parsed template. * return true if successful. otherwise return false. */ bool Cstore::getParsedTmpl(const vector& path_comps, Cstore::MapT& tmap, bool allow_val) { vtw_def def; /* currently this function is used outside actual CLI operations, mainly * from the perl API. since value validation is from the original CLI * implementation, it doesn't seem to behave correctly in such cases, * probably because "at string" is not set? * * anyway, not validating values in the following call. */ if (!get_parsed_tmpl(path_comps, false, def)) { return false; } if (!allow_val && def.is_value) { /* note: !allow_val means specified path must terminate at an actual * "node", not a "value". so this fails since path ends in value. * this emulates the original perl API behavior. */ return false; } if (def.is_value) { tmap["is_value"] = "1"; } // make the map if (def.def_type != ERROR_TYPE) { tmap["type"] = type_to_name(def.def_type); } if (def.def_type2 != ERROR_TYPE) { tmap["type2"] = type_to_name(def.def_type2); } if (def.def_node_help) { tmap["help"] = def.def_node_help; } if (def.multi) { tmap["multi"] = "1"; if (def.def_multi > 0) { ostringstream s; s << def.def_multi; tmap["limit"] = s.str(); } } else if (def.tag) { tmap["tag"] = "1"; if (def.def_tag > 0) { ostringstream s; s << def.def_tag; tmap["limit"] = s.str(); } } else if (def.def_default) { tmap["default"] = def.def_default; } if (def.def_enumeration) { tmap["enum"] = def.def_enumeration; } if (def.def_allowed) { tmap["allowed"] = def.def_allowed; } if (def.def_val_help) { tmap["val_help"] = def.def_val_help; } return true; } /* get names of all template child nodes of specified path. * cnodes: (output) template child node names. * note: if specified path is at a "tag node", "node.tag" will be returned. */ void Cstore::tmplGetChildNodes(const vector& path_comps, vector& cnodes) { SAVE_PATHS; append_tmpl_path(path_comps); get_all_tmpl_child_node_names(cnodes); sort_nodes(cnodes); RESTORE_PATHS; } /* delete specified "logical path" from "working config". * def: parsed template corresponding to logical path path_comps. * return true if successful. otherwise return false. * note: assume specified path has been validated * (i.e., validateDeletePath()). */ bool Cstore::deleteCfgPath(const vector& path_comps, const vtw_def& def) { ASSERT_IN_SESSION; if (!cfg_path_exists(path_comps, false, true)) { output_user("Nothing to delete (the specified %s does not exist)\n", (!def.is_value || def.tag) ? "node" : "value"); // treat as success return true; } /* path already validated and in working config. * cases: * 1. has default value * => replace current value with default * 2. no default value * => remove config path */ if (def.def_default) { // case 1. construct path for value file. SAVE_PATHS; append_cfg_path(path_comps); if (def.is_value) { // last comp is "value". need to go up 1 level. pop_cfg_path(); } /* assume default value is valid (parser should have validated). * also call unmark_deactivated() in case the node being deleted was * also deactivated. note that unmark_deactivated() succeeds if it's * not marked deactivated. also mark "changed". */ bool ret = (write_value(def.def_default) && mark_display_default() && unmark_deactivated() && mark_changed_with_ancestors()); if (!ret) { output_user("Failed to set default value during delete\n"); } RESTORE_PATHS; return ret; } /* case 2. * sub-cases: * (1) last path comp is "value", i.e., tag (value of tag node), * value of single-value node, or value of multi-value node. * (a) value of single-value node * => remove node * (b) value of multi-value node * => remove value. remove node if last value. * (c) value of tag node (i.e., tag) * => remove tag. remove node if last tag. * (2) last path comp is "node", i.e., typeless node, tag node, * single-value node, or multi-value node. * => remove node */ bool ret = false; SAVE_PATHS; append_cfg_path(path_comps); if (!def.is_value) { // sub-case (2) ret = remove_node(); } else { // last comp is value if (def.tag) { // sub-case (1c) ret = remove_tag(); } else if (def.multi) { // sub-case (1b) pop_cfg_path(); ret = remove_value_from_multi(path_comps[path_comps.size() - 1]); } else { // sub-case (1a). delete node at 1 level up. pop_cfg_path(); ret = remove_node(); } } if (ret) { // mark changed ret = mark_changed_with_ancestors(); } RESTORE_PATHS; if (!ret) { output_user("Failed to delete specified config path\n"); } return ret; } /* check if specified "logical path" is valid for "set" operation * return true if valid. otherwise return false. */ bool Cstore::validateSetPath(const vector& path_comps) { ASSERT_IN_SESSION; // if we can get parsed tmpl, path is valid vtw_def def; string terr; if (!get_parsed_tmpl(path_comps, true, def, terr)) { output_user("%s\n", terr.c_str()); return false; } bool ret = true; SAVE_PATHS; if (!def.is_value) { if (def.def_type != ERROR_TYPE) { /* disallow setting value node without value * note: different from old behavior, which only disallow setting a * single-value node without value. now all value nodes * (single-value, multi-value, and tag) must be set with value. */ output_user("The specified configuration node requires a value\n"); ret = false; } else { /* typeless node * note: XXX the following is present in the original logic, perhaps * to trigger check_syn() on the typeless node? is this really * necessary? * also, validate_val() uses current cfg path and tmpl path, so * construct them before calling it. */ append_cfg_path(path_comps); append_tmpl_path(path_comps); if (!validate_val(&def, "")) { ret = false; } } } RESTORE_PATHS; return ret; } /* check if specified "logical path" is valid for "delete" operation * return true if valid. otherwise return false. */ bool Cstore::validateDeletePath(const vector& path_comps, vtw_def& def) { ASSERT_IN_SESSION; string terr; if (!get_parsed_tmpl(path_comps, false, def, terr)) { output_user("%s\n", terr.c_str()); return false; } return true; } /* check if specified "logical path" is valid for "activate" operation * return true if valid. otherwise return false. */ bool Cstore::validateActivatePath(const vector& path_comps) { ASSERT_IN_SESSION; vtw_def def; if (!validate_act_deact(path_comps, "activate", def)) { return false; } if (!cfgPathMarkedDeactivated(path_comps)) { output_user("Activate can only be performed on a node on which the " "deactivate\ncommand has been performed.\n"); return false; } bool ret = true; if (def.is_value && def.tag && def.def_tag > 0) { // we are activating a tag, and there is a limit on number of tags. vector cnodes; SAVE_PATHS; append_cfg_path(path_comps); string t = pop_cfg_path(); // get child nodes, excluding deactivated ones. get_all_child_node_names(cnodes, false, false); if (def.def_tag <= cnodes.size()) { // limit exceeded output_user("Cannot activate \"%s\": number of values exceeds limit " "(%d allowed)\n", t.c_str(), def.def_tag); ret = false; } RESTORE_PATHS; } return ret; } /* check if specified "logical path" is valid for "deactivate" operation * return true if valid. otherwise return false. */ bool Cstore::validateDeactivatePath(const vector& path_comps) { ASSERT_IN_SESSION; vtw_def def; return validate_act_deact(path_comps, "deactivate", def); } /* check if specified "logical path" is valid for "edit" operation. * return false if invalid. * if valid, set "env" arg to the environment string needed for the "edit" * operation and return true. */ bool Cstore::getEditEnv(const vector& path_comps, string& env) { ASSERT_IN_SESSION; vtw_def def; string terr; if (!get_parsed_tmpl(path_comps, false, def, terr)) { output_user("%s\n", terr.c_str()); return false; } /* "edit" is only allowed when path ends at a * (1) "tag value" * OR * (2) "typeless node" */ if (!(def.is_value && def.tag) && !(!def.is_value && def.def_type == ERROR_TYPE)) { // neither "tag value" nor "typeless node" output_user("The \"edit\" command cannot be issued " "at the specified level\n"); return false; } if (!cfg_path_exists(path_comps, false, true)) { /* specified path does not exist. * follow the original implementation and do a "set". */ if (!validateSetPath(path_comps)) { output_user("The specified config path is not valid\n"); return false; } if (!set_cfg_path(path_comps, false)) { output_user("Failed to create the specified config path\n"); return false; } } SAVE_PATHS; append_cfg_path(path_comps); append_tmpl_path(path_comps); get_edit_env(env); RESTORE_PATHS; /* doing the save/restore above to be consistent with the rest of the API. * however, after the caller evals the returned environment string, the * levels in "this" will become out-of-sync with the environment. so * "this" should no longer be used and a new object should be created. * * this is only an issue if the calling process doesn't terminate. since * the function should only be used by the shell/completion, it's not a * problem (each invocation of my_cli_shell_api uses a new object anyway). */ return true; } /* set "env" arg to the environment string needed for the "up" operation. * return true if successful. */ bool Cstore::getEditUpEnv(string& env) { ASSERT_IN_SESSION; /* "up" is based on current levels in environment. levels should already * be set up in constructor (with "use_edit_level" true). */ if (edit_level_at_root()) { output_user("Already at the top level\n"); return false; } /* get_parsed_tmpl() does not allow empty path, so use one component * from current paths. */ vtw_def def; string terr; vector path_comps; if (!get_parsed_tmpl(path_comps, false, def, terr)) { // this should not happen since it's using existing levels output_user("%s\n", terr.c_str()); return false; } SAVE_PATHS; if (def.is_value && def.tag) { // edit level is at "tag value". go up 1 extra level. pop_cfg_path(); pop_tmpl_path(); } pop_cfg_path(); pop_tmpl_path(); get_edit_env(env); RESTORE_PATHS; // also see getEditEnv for comment on save/restore above return true; } /* set "env" arg to the environment string needed to reset edit levels. * return true if successful. */ bool Cstore::getEditResetEnv(string& env) { ASSERT_IN_SESSION; SAVE_PATHS; while (!edit_level_at_root()) { pop_cfg_path(); pop_tmpl_path(); } get_edit_env(env); RESTORE_PATHS; // also see getEditEnv for comment on save/restore above return true; } /* set "env" arg to the environment string needed for "completion". * return true if successful. * * note: comps must have at least 2 components, the "command" and the * first path element (which can be empty string). */ bool Cstore::getCompletionEnv(const vector& comps, string& env) { ASSERT_IN_SESSION; vector pcomps = comps; string cmd = pcomps[0]; string last_comp = pcomps.back(); pcomps.erase(pcomps.begin()); pcomps.pop_back(); bool exists_only = (cmd == "delete" || cmd == "show" || cmd == "edit" || cmd == "comment" || cmd == "activate" || cmd == "deactivate"); /* at this point, pcomps contains the command line arguments minus the * "command" and the last one. */ bool ret = false; SAVE_PATHS; do { vtw_def def; if (pcomps.size() > 0) { if (!get_parsed_tmpl(pcomps, false, def)) { // invalid path break; } if (exists_only && !cfg_path_exists(pcomps, false, true)) { // invalid path for the command (must exist) break; } append_cfg_path(pcomps); append_tmpl_path(pcomps); } else { // we are at root. simulate a typeless node. def.def_type = ERROR_TYPE; def.tag = def.multi = def.is_value = 0; } /* at this point, cfg and tmpl paths are constructed up to the comp * before last_comp, and def is parsed. */ if (def.is_value && !def.tag) { // invalid path (this means the comp before last_comp is a leaf value) break; } vector comp_vals; string comp_string; string comp_help; vector > help_pairs; bool last_comp_val = true; if (def.def_type == ERROR_TYPE || def.is_value) { /* path so far is at a typeless node OR a tag value (tag already * checked above): * completions: from tmpl children. * help: * values: same as completions. * text: "help" from child templates. * * note: for such completions, we filter non-existent nodes if * necessary. */ vector ufvec; if (exists_only) { // only return existing config nodes get_all_child_node_names(ufvec, false, true); } else { // return all template children get_all_tmpl_child_node_names(ufvec); } for (size_t i = 0; i < ufvec.size(); i++) { if (last_comp == "" || ufvec[i].compare(0, last_comp.length(), last_comp) == 0) { comp_vals.push_back(ufvec[i]); } } if (comp_vals.size() == 0) { // no matches break; } sort(comp_vals.begin(), comp_vals.end()); for (size_t i = 0; i < comp_vals.size(); i++) { pair hpair(comp_vals[i], ""); push_tmpl_path(hpair.first); vtw_def cdef; if (tmpl_parse(cdef) && cdef.def_node_help) { hpair.second = cdef.def_node_help; } else { hpair.second = ""; } help_pairs.push_back(hpair); pop_tmpl_path(); } // last comp is not value last_comp_val = false; } else { /* path so far is at a "value node". * note: follow the original implementation and don't filter * non-existent values for such completions */ // first, handle completions. if (def.tag) { // it's a "tag node". get completions from tag values. get_all_child_node_names(comp_vals, false, true); } else { // it's a "leaf value node". get completions from values. read_value_vec(comp_vals, false); } /* more possible completions from this node's template: * "allowed" * "enumeration" * "$VAR(@) in ..." */ if (def.def_enumeration || def.def_allowed) { /* do "enumeration" or "allowed". * note: emulate original implementation and set up COMP_WORDS and * COMP_CWORD environment variables. these are needed by some * "allowed" scripts. */ ostringstream cword_count; cword_count << (comps.size() - 1); string cmd_str = ("export " + C_ENV_SHELL_CWORD_COUNT + "=" + cword_count.str() + "; "); cmd_str += ("export " + C_ENV_SHELL_CWORDS + "=("); for (size_t i = 0; i < comps.size(); i++) { cmd_str += (" '" + comps[i] + "'"); } cmd_str += "); "; if (def.def_enumeration) { cmd_str += (C_ENUM_SCRIPT_DIR + "/" + def.def_enumeration); } else { string astr = def.def_allowed; shell_escape_squotes(astr); cmd_str += "_cstore_internal_allowed () { eval '"; cmd_str += astr; cmd_str += "'; }; _cstore_internal_allowed"; } char *buf = (char *) malloc(MAX_CMD_OUTPUT_SIZE); int ret = get_shell_command_output(cmd_str.c_str(), buf, MAX_CMD_OUTPUT_SIZE); if (ret > 0) { // '<' and '>' need to be escaped char *ptr = buf; while (*ptr) { if (*ptr == '<' || *ptr == '>') { comp_string += "\\"; } comp_string += *ptr; ptr++; } } /* note that for "enumeration" and "allowed", comp_string is the * complete output of the command and it is to be evaled by the * shell into an array of values. */ free(buf); } else if (def.actions[syntax_act].vtw_list_head) { // look for "self ref in values" from syntax const valstruct *vals = get_syntax_self_in_valstruct( def.actions[syntax_act].vtw_list_head); if (vals) { if (vals->cnt == 0 && vals->val) { comp_vals.push_back(vals->val); } else if (vals->cnt > 0) { for (int i = 0; i < vals->cnt; i++) { if (vals->vals[i]) { comp_vals.push_back(vals->vals[i]); } } } } } // now handle help. if (def.def_comp_help) { // "comp_help" exists. comp_help = def.def_comp_help; shell_escape_squotes(comp_help); } if (def.def_val_help) { // has val_help. first separate individual lines. size_t start = 0, i = 0; vector vhelps; for (i = 0; def.def_val_help[i]; i++) { if (def.def_val_help[i] == '\n') { vhelps.push_back(string(&(def.def_val_help[start]), i - start)); start = i + 1; } } if (start < i) { vhelps.push_back(string(&(def.def_val_help[start]), i - start)); } // process each line for (i = 0; i < vhelps.size(); i++) { size_t sc; if ((sc = vhelps[i].find(';')) == vhelps[i].npos) { // no ';' if (i == 0 && def.def_type != ERROR_TYPE) { // first val_help. pair with "type". help_pairs.push_back(pair( type_to_name(def.def_type), vhelps[i])); } if (i == 1 && def.def_type2 != ERROR_TYPE) { // second val_help. pair with "type2". help_pairs.push_back(pair( type_to_name(def.def_type2), vhelps[i])); } } else { // ';' at index sc help_pairs.push_back(pair( vhelps[i].substr(0, sc), vhelps[i].substr(sc + 1))); } } } else if (def.def_type && def.def_node_help) { // simple case. just use "type" and "help" help_pairs.push_back(pair(type_to_name(def.def_type), def.def_node_help)); } } // this var is the array of possible completions env = (C_ENV_SHAPI_COMP_VALS + "=("); for (size_t i = 0; i < comp_vals.size(); i++) { shell_escape_squotes(comp_vals[i]); env += ("'" + comp_vals[i] + "' "); } /* as mentioned above, comp_string is the complete command output. * let the shell eval it into the array since we don't want to * re-implement the shell interpretation here. * * note that as a result, we will not be doing the filtering here. * instead, the completion script will do the filtering on * the resulting comp_values array. should be straightforward since * there's no "existence filtering", only "prefix filtering". */ env += (comp_string + "); "); /* this var indicates whether the last comp is "value" * follow original implementation: if last comp is value, completion * script needs to do the following. * use comp_help if exists * prefix filter comp_values * replace any <*> in comp_values with "" * convert help items to data representation */ env += (C_ENV_SHAPI_LCOMP_VAL + "="); env += (last_comp_val ? "true; " : "false; "); // this var is the "comp_help" string env += (C_ENV_SHAPI_COMP_HELP + "='" + comp_help + "'; "); // this var is the array of "help items", i.e., type names, etc. string hitems = (C_ENV_SHAPI_HELP_ITEMS + "=("); // this var is the array of "help strings" corresponding to the items string hstrs = (C_ENV_SHAPI_HELP_STRS + "=("); for (size_t i = 0; i < help_pairs.size(); i++) { string hi = help_pairs[i].first; string hs = help_pairs[i].second; shell_escape_squotes(hi); shell_escape_squotes(hs); // get rid of leading/trailing "space" chars in help string while (hi.size() > 0 && isspace(hi[0])) { hi.erase(0, 1); } while (hs.size() > 0 && isspace(hs[0])) { hs.erase(0, 1); } while (hi.size() > 0 && isspace(hi[hi.size() - 1])) { hi.erase(hi.size() - 1); } while (hs.size() > 0 && isspace(hs[hs.size() - 1])) { hs.erase(hs.size() - 1); } hitems += ("'" + hi + "' "); hstrs += ("'" + hs + "' "); } env += (hitems + "); " + hstrs + "); "); ret = true; } while(0); RESTORE_PATHS; return ret; } /* set specified "logical path" in "working config". * return true if successful. otherwise return false. * note: assume specified path is valid (i.e., validateSetPath()). */ bool Cstore::setCfgPath(const vector& path_comps) { ASSERT_IN_SESSION; return set_cfg_path(path_comps, true); } /* check if specified "arguments" is valid for "rename" operation * return true if valid. otherwise return false. */ bool Cstore::validateRenameArgs(const vector& args) { ASSERT_IN_SESSION; return validate_rename_copy(args, "rename"); } /* check if specified "arguments" is valid for "copy" operation * return true if valid. otherwise return false. */ bool Cstore::validateCopyArgs(const vector& args) { ASSERT_IN_SESSION; return validate_rename_copy(args, "copy"); } /* check if specified "arguments" is valid for "move" operation * return true if valid. otherwise return false. */ bool Cstore::validateMoveArgs(const vector& args) { ASSERT_IN_SESSION; vector epath; vector nargs; if (!conv_move_args_for_rename(args, epath, nargs)) { output_user("Invalid move command\n"); return false; } SAVE_PATHS; append_cfg_path(epath); append_tmpl_path(epath); bool ret = validate_rename_copy(nargs, "move"); RESTORE_PATHS; return ret; } /* check if specified "arguments" is valid for "comment" operation * return true if valid. otherwise return false. */ bool Cstore::validateCommentArgs(const vector& args, vtw_def& def) { ASSERT_IN_SESSION; /* separate path from comment. * follow the original implementation: the last arg is the comment, and * everything else is part of the path. */ vector path_comps(args); string comment = args.back(); path_comps.pop_back(); // check the path string terr; if (!get_parsed_tmpl(path_comps, false, def, terr)) { output_user("%s\n", terr.c_str()); return false; } // here we want to include deactivated nodes if (!cfg_path_exists(path_comps, false, true)) { output_user("The specified config node does not exist\n"); return false; } if (def.is_value && !def.tag) { /* XXX differ from the original implementation, which allows commenting * on a "value" BUT silently "promote" the comment to the parent * "node". this will probably create confusion for the user. * * just disallow such cases here. */ output_user("Cannot comment on config values\n"); return false; } if (def.tag && !def.is_value) { /* XXX follow original implementation and disallow comment on a * "tag node". this is because "show" does not display such * comments (see bug 5794). */ output_user("Cannot add comment at this level\n"); return false; } if (comment.find_first_of('*') != string::npos) { // don't allow '*'. this is due to config files using C-style /**/ // comments. this probably belongs to lower-level, but we are enforcing // it here. output_user("Cannot use the '*' character in a comment\n"); return false; } return true; } /* perform rename in "working config" according to specified args. * return true if successful. otherwise return false. * note: assume args are already validated (i.e., validateRenameArgs()). */ bool Cstore::renameCfgPath(const vector& args) { ASSERT_IN_SESSION; string otagnode = args[0]; string otagval = args[1]; string ntagval = args[4]; push_cfg_path(otagnode); /* also mark changed. note that it's marking the "tag node" but not the * "tag values" since one is being "deleted" and the other is being * "added" anyway. */ bool ret = (rename_child_node(otagval, ntagval) && mark_changed_with_ancestors()); pop_cfg_path(); return ret; } /* perform copy in "working config" according to specified args. * return true if successful. otherwise return false. * note: assume args are already validated (i.e., validateCopyArgs()). */ bool Cstore::copyCfgPath(const vector& args) { ASSERT_IN_SESSION; string otagnode = args[0]; string otagval = args[1]; string ntagval = args[4]; push_cfg_path(otagnode); /* also mark changed. note that it's marking the "tag node" but not the * new "tag value" since it is being "added" anyway. */ bool ret = (copy_child_node(otagval, ntagval) && mark_changed_with_ancestors()); pop_cfg_path(); return ret; } /* perform "comment" in working config according to specified args. * return true if valid. otherwise return false. */ bool Cstore::commentCfgPath(const vector& args, const vtw_def& def) { ASSERT_IN_SESSION; // separate path from comment vector path_comps(args); string comment = args.back(); path_comps.pop_back(); SAVE_PATHS; append_cfg_path(path_comps); bool ret; if (comment == "") { // follow original impl: empty comment => remove it ret = remove_comment(); if (!ret) { output_user("Failed to remove comment for specified config node\n"); } } else { ret = set_comment(comment); if (!ret) { output_user("Failed to add comment for specified config node\n"); } } RESTORE_PATHS; if (ret) { // mark the root as changed for "comment" ret = mark_changed_with_ancestors(); } return ret; } /* discard all changes in working config. * return true if successful. otherwise return false. */ bool Cstore::discardChanges() { ASSERT_IN_SESSION; // just call underlying implementation unsigned long long num_removed = 0; if (discard_changes(num_removed)) { if (num_removed > 0) { output_user("Changes have been discarded\n"); } else { output_user("No changes have been discarded\n"); } return true; } return false; } /* perform move in "working config" according to specified args. * return true if successful. otherwise return false. * note: assume args are already validated (i.e., validateMoveArgs()). */ bool Cstore::moveCfgPath(const vector& args) { ASSERT_IN_SESSION; vector epath; vector nargs; if (!conv_move_args_for_rename(args, epath, nargs)) { output_user("Invalid move command\n"); return false; } SAVE_PATHS; append_cfg_path(epath); append_tmpl_path(epath); bool ret = renameCfgPath(nargs); RESTORE_PATHS; return ret; } /* check if specified "logical path" exists in working config (i.e., the union) * or active config (i.e., the original). * return true if it exists. otherwise return false. */ bool Cstore::cfgPathExists(const vector& path_comps, bool active_cfg) { if (!active_cfg) { ASSERT_IN_SESSION; } return cfg_path_exists(path_comps, active_cfg, false); } // same as above but "deactivate-aware" bool Cstore::cfgPathExistsDA(const vector& path_comps, bool active_cfg, bool include_deactivated) { if (!active_cfg) { ASSERT_IN_SESSION; } return cfg_path_exists(path_comps, active_cfg, include_deactivated); } /* check if specified "logical path" has been deleted in working config. */ bool Cstore::cfgPathDeleted(const vector& path_comps) { ASSERT_IN_SESSION; // whether it's in active but not in working return (cfg_path_exists(path_comps, true, false) && !cfg_path_exists(path_comps, false, false)); } /* check if specified "logical path" has been added in working config. */ bool Cstore::cfgPathAdded(const vector& path_comps) { ASSERT_IN_SESSION; // whether it's not in active but in working return (!cfg_path_exists(path_comps, true, false) && cfg_path_exists(path_comps, false, false)); } /* check if specified "logical path" has been "changed" in working config. * XXX the definition of "changed" is different from the original * perl API implementation isChanged(), which was inconsistent between * "deleted" and "deactivated". * * original logic (with $disable arg not defined) returns true in * either of the 2 cases below: * (1) node is BEING deactivated or activated * (2) node appears in changes_only dir * which means it returns false for nodes being deleted but true * for nodes being deactivated. * * new logic returns true if any of the following is true * (remember this functions is NOT "deactivate-aware") * (1) cfgPathDeleted() * (2) cfgPathAdded() * (3) cfg_node_changed() */ bool Cstore::cfgPathChanged(const vector& path_comps) { ASSERT_IN_SESSION; if (cfgPathDeleted(path_comps) || cfgPathAdded(path_comps)) { return true; } SAVE_PATHS; append_cfg_path(path_comps); bool ret = cfg_node_changed(); RESTORE_PATHS; return ret; } /* get names of "deleted" child nodes of specified path during commit * operation. names are returned in cnodes. */ void Cstore::cfgPathGetDeletedChildNodes(const vector& path_comps, vector& cnodes) { ASSERT_IN_SESSION; cfgPathGetDeletedChildNodesDA(path_comps, cnodes, false); } // same as above but "deactivate-aware" void Cstore::cfgPathGetDeletedChildNodesDA(const vector& path_comps, vector& cnodes, bool include_deactivated) { ASSERT_IN_SESSION; vector acnodes; cfgPathGetChildNodesDA(path_comps, acnodes, true, include_deactivated); vector wcnodes; cfgPathGetChildNodesDA(path_comps, wcnodes, false, include_deactivated); MapT cmap; for (size_t i = 0; i < wcnodes.size(); i++) { cmap[wcnodes[i]] = true; } for (size_t i = 0; i < acnodes.size(); i++) { if (cmap.find(acnodes[i]) == cmap.end()) { // in active but not in working cnodes.push_back(acnodes[i]); } } sort_nodes(cnodes); } /* get "deleted" values of specified "multi node" during commit * operation. values are returned in dvals. if specified path is not * a "multi node", it's a nop. * * NOTE: this function does not consider the "value ordering". the "deleted" * status is purely based on the presence/absence of a value. */ void Cstore::cfgPathGetDeletedValues(const vector& path_comps, vector& dvals) { ASSERT_IN_SESSION; cfgPathGetDeletedValuesDA(path_comps, dvals, false); } // same as above but DA void Cstore::cfgPathGetDeletedValuesDA(const vector& path_comps, vector& dvals, bool include_deactivated) { ASSERT_IN_SESSION; vector ovals; vector nvals; if (!cfgPathGetValuesDA(path_comps, ovals, true, include_deactivated) || !cfgPathGetValuesDA(path_comps, nvals, false, include_deactivated)) { return; } MapT dmap; for (size_t i = 0; i < nvals.size(); i++) { dmap[nvals[i]] = true; } for (size_t i = 0; i < ovals.size(); i++) { if (dmap.find(ovals[i]) == dmap.end()) { // in active but not in working dvals.push_back(ovals[i]); } } } /* check whether specified path is "deactivated" in working config or * active config. * a node is "deactivated" if the node itself or any of its ancestors is * "marked deactivated". */ bool Cstore::cfgPathDeactivated(const vector& path_comps, bool active_cfg) { if (!active_cfg) { ASSERT_IN_SESSION; } vector ppath; for (size_t i = 0; i < path_comps.size(); i++) { ppath.push_back(path_comps[i]); if (cfgPathMarkedDeactivated(ppath, active_cfg)) { // an ancestor or itself is marked deactivated return true; } } return false; } /* check whether specified path is "marked deactivated" in working config or * active config. * a node is "marked deactivated" if a deactivate operation has been * performed on the node. */ bool Cstore::cfgPathMarkedDeactivated(const vector& path_comps, bool active_cfg) { if (!active_cfg) { ASSERT_IN_SESSION; } SAVE_PATHS; append_cfg_path(path_comps); bool ret = marked_deactivated(active_cfg); RESTORE_PATHS; return ret; } /* get names of child nodes of specified path in working config or active * config. names are returned in cnodes. */ void Cstore::cfgPathGetChildNodes(const vector& path_comps, vector& cnodes, bool active_cfg) { if (!active_cfg) { ASSERT_IN_SESSION; } cfgPathGetChildNodesDA(path_comps, cnodes, active_cfg, false); } // same as above but "deactivate-aware" void Cstore::cfgPathGetChildNodesDA(const vector& path_comps, vector& cnodes, bool active_cfg, bool include_deactivated) { if (!active_cfg) { ASSERT_IN_SESSION; } if (!include_deactivated && cfgPathDeactivated(path_comps, active_cfg)) { /* this node is deactivated (an ancestor or this node itself is * marked deactivated) and we don't want to include deactivated. nop. */ return; } SAVE_PATHS; append_cfg_path(path_comps); get_all_child_node_names(cnodes, active_cfg, include_deactivated); RESTORE_PATHS; sort_nodes(cnodes); } /* get value of specified single-value node. * value: (output) node value. * active_cfg: whether to get value from active config. * return false if fails (invalid node, doesn't exist, read fails, etc.). * otherwise return true. */ bool Cstore::cfgPathGetValue(const vector& path_comps, string& value, bool active_cfg) { if (!active_cfg) { ASSERT_IN_SESSION; } return cfgPathGetValueDA(path_comps, value, active_cfg, false); } // same as above but "deactivate-aware" bool Cstore::cfgPathGetValueDA(const vector& path_comps, string& value, bool active_cfg, bool include_deactivated) { if (!active_cfg) { ASSERT_IN_SESSION; } vtw_def def; if (!get_parsed_tmpl(path_comps, false, def)) { // invalid node return false; } /* note: the behavior here is different from original perl API, which * does not check if specified node is indeed single-value. so if * the function is erroneously used on a multi-value node, the * original API will return a single string that includes all values. * this new function will return failure in such cases. */ if (def.is_value || def.multi || def.tag || def.def_type == ERROR_TYPE) { // specified path is not a single-value node return false; } if (!cfg_path_exists(path_comps, active_cfg, include_deactivated)) { // specified node doesn't exist return false; } vector vvec; SAVE_PATHS; append_cfg_path(path_comps); bool ret = false; if (read_value_vec(vvec, active_cfg)) { if (vvec.size() >= 1) { // if for some reason we got multiple values, just take the first one. value = vvec[0]; ret = true; } } RESTORE_PATHS; return ret; } /* get values of specified multi-value node. * values: (output) node values. * active_cfg: whether to get values from active config. * return false if fails (invalid node, doesn't exist, etc.). * otherwise return true. */ bool Cstore::cfgPathGetValues(const vector& path_comps, vector& values, bool active_cfg) { if (!active_cfg) { ASSERT_IN_SESSION; } return cfgPathGetValuesDA(path_comps, values, active_cfg, false); } // same as above but "deactivate-aware" bool Cstore::cfgPathGetValuesDA(const vector& path_comps, vector& values, bool active_cfg, bool include_deactivated) { if (!active_cfg) { ASSERT_IN_SESSION; } vtw_def def; if (!get_parsed_tmpl(path_comps, false, def)) { // invalid node return false; } /* note: the behavior here is different from original perl API, which * does not check if specified node is indeed multi-value. so if * the function is erroneously used on a single-value node, the * original API will return the node's value. this new function * will return failure in such cases. */ if (def.is_value || !def.multi || def.tag || def.def_type == ERROR_TYPE) { // specified path is not a multi-value node return false; } if (!cfg_path_exists(path_comps, active_cfg, include_deactivated)) { // specified node doesn't exist return false; } SAVE_PATHS; append_cfg_path(path_comps); bool ret = read_value_vec(values, active_cfg); RESTORE_PATHS; return ret; } /* get comment of specified node. * comment: (output) node comment. * active_cfg: whether to get comment from active config. * return false if fails (invalid node, doesn't exist, etc.). * otherwise return true. */ bool Cstore::cfgPathGetComment(const vector& path_comps, string& comment, bool active_cfg) { if (!active_cfg) { ASSERT_IN_SESSION; } SAVE_PATHS; append_cfg_path(path_comps); bool ret = get_comment(comment, active_cfg); RESTORE_PATHS; return ret; } /* return whether specified path is "default". if a node is "default", it * is currently not shown by the "show" command unless "-all" is specified. * active_cfg: whether to observe active config. */ bool Cstore::cfgPathDefault(const vector& path_comps, bool active_cfg) { if (!active_cfg) { ASSERT_IN_SESSION; } SAVE_PATHS; append_cfg_path(path_comps); bool ret = marked_display_default(active_cfg); RESTORE_PATHS; return ret; } /* the following functions are observers of the "effective" config. * they can be used * (1) outside a config session (e.g., op mode, daemons, callbacks, etc.). * OR * (2) during a config session * * HOWEVER, NOTE that the definition of "effective" is different under these * two scenarios. * (1) when used outside a config session, "effective" == "active". * in other words, in such cases the effective config is the same * as the running config. * * (2) when used during a config session, a config path (leading to either * a "node" or a "value") is "effective" if ANY of the following * is true. * (a) active && working * path is in both active and working configs, i.e., unchanged. * (b) !active && working && committed * path is not in active, has been set in working, AND has * already been committed, i.e., "commit" has successfully * processed the addition/update of the path. * (c) active && !working && !committed * path is in active, has been deleted from working, AND * has not been committed yet, i.e., "commit" (per priority) has * not processed the deletion of the path yet, or it has been * processed but failed. * * note: during commit, deactivate has the same effect as delete. so * in such cases, as far as these functions are concerned, * deactivated nodes don't exist. * * originally, these functions are exclusively for use during config * sessions. however, for some usage scenarios, it is useful to have a set * of API functions that can be used both during and outside config * sessions. therefore, definition (1) is added above for convenience. * * for example, a developer can use these functions in a script that can * be used both during a commit action and outside config mode, as long as * the developer is clearly aware of the difference between the above two * definitions. * * note that when used outside a config session (i.e., definition (1)), * these functions are equivalent to the observers for the "active" config. * * to avoid any confusion, when possible (e.g., in a script that is * exclusively used in op mode), developers should probably use those * "active" observers explicitly when outside a config session instead * of these "effective" observers. * * it is also important to note that when used outside a config session, * due to race conditions, it is possible that the "observed" active config * becomes out-of-sync with the config that is actually "in effect". * specifically, this happens when two things occur simultaneously: * (a) an observer function is called from outside a config session. * AND * (b) someone invokes "commit" inside a config session (any session). * * this is because "commit" only updates the active config at the end after * all commit actions have been executed, so before the update happens, * some config nodes have already become "effective" but are not yet in the * "active config" and therefore are not observed by these functions. * * note that this is only a problem when the caller is outside config mode. * in such cases, the caller (which could be an op-mode command, a daemon, * a callback script, etc.) already must be able to handle config changes * that can happen at any time. if "what's configured" is more important, * using the "active config" should be fine as long as it is relatively * up-to-date. if the actual "system state" is more important, then the * caller should probably just check the system state in the first place * (instead of using these config observers). * * one possible solution is for these "effective" observers to obtain the * global commit lock before returning their observations. this has not * been implemented yet since the impact of this issue is not clear at * the moment. */ // return whether specified path is "effective". bool Cstore::cfgPathEffective(const vector& path_comps) { vtw_def def; if (!validateTmplPath(path_comps, false, def)) { // invalid path return false; } bool in_active = cfg_path_exists(path_comps, true, false); if (!inSession()) { // not in a config session. use active config only. return in_active; } bool in_work = cfg_path_exists(path_comps, false, false); if (in_active && in_work) { // case (1) return true; } bool ret = false; SAVE_PATHS; append_cfg_path(path_comps); if (!in_active && in_work) { // check if case (2) ret = marked_committed(def, true); } else if (in_active && !in_work) { // check if case (3) ret = !marked_committed(def, false); } RESTORE_PATHS; return ret; } /* get names of "effective" child nodes of specified path during commit * operation. see above function for definition of "effective". * names are returned in cnodes. */ void Cstore::cfgPathGetEffectiveChildNodes(const vector& path_comps, vector& cnodes) { if (!inSession()) { // not in a config session. use active config only. cfgPathGetChildNodes(path_comps, cnodes, true); return; } // get a union of active and working MapT cmap; vector acnodes; vector wcnodes; cfgPathGetChildNodes(path_comps, acnodes, true); cfgPathGetChildNodes(path_comps, wcnodes, false); for (size_t i = 0; i < acnodes.size(); i++) { cmap[acnodes[i]] = true; } for (size_t i = 0; i < wcnodes.size(); i++) { cmap[wcnodes[i]] = true; } // get only the effective ones from the union vector ppath = path_comps; MapT::iterator it = cmap.begin(); for (; it != cmap.end(); ++it) { string c = (*it).first; ppath.push_back(c); if (cfgPathEffective(ppath)) { cnodes.push_back(c); } ppath.pop_back(); } sort_nodes(cnodes); } /* get the "effective" value of specified path during commit operation. * value: (output) node value * return true if successful. otherwise return false. */ bool Cstore::cfgPathGetEffectiveValue(const vector& path_comps, string& value) { if (!inSession()) { // not in a config session. use active config only. return cfgPathGetValue(path_comps, value, true); } vector ppath = path_comps; string oval, nval; bool oret = cfgPathGetValue(path_comps, oval, true); bool nret = cfgPathGetValue(path_comps, nval, false); bool ret = false; // all 4 combinations of oret and nret are covered below if (nret) { // got new value ppath.push_back(nval); if (cfgPathEffective(ppath)) { // nval already effective value = nval; ret = true; } else if (!oret) { // no oval. failure. } else { // oval still effective value = oval; ret = true; } } else if (oret) { // got oval only ppath.push_back(oval); if (cfgPathEffective(ppath)) { // oval still effective value = oval; ret = true; } } return ret; } /* get the "effective" values of specified path during commit operation. * values: (output) node values * return true if successful. otherwise return false. */ bool Cstore::cfgPathGetEffectiveValues(const vector& path_comps, vector& values) { if (!inSession()) { // not in a config session. use active config only. cfgPathGetValues(path_comps, values, true); return (values.size() > 0); } // get a union of active and working MapT vmap; vector ovals; vector nvals; cfgPathGetValues(path_comps, ovals, true); cfgPathGetValues(path_comps, nvals, false); for (size_t i = 0; i < ovals.size(); i++) { vmap[ovals[i]] = true; } for (size_t i = 0; i < nvals.size(); i++) { vmap[nvals[i]] = true; } // get only the effective ones from the union vector ppath = path_comps; MapT::iterator it = vmap.begin(); for (; it != vmap.end(); ++it) { string c = (*it).first; ppath.push_back(c); if (cfgPathEffective(ppath)) { values.push_back(c); } ppath.pop_back(); } return (values.size() > 0); } /* get the value string that corresponds to specified variable ref string. * ref_str: var ref string (e.g., "./cost/@"). * type: (output) the node type. * from_active: if true, value string should come from "active config". * otherwise from "working config". * return a pointer to the value string if successful (caller must free). * otherwise return NULL. */ char * Cstore::getVarRef(const string& ref_str, vtw_type_e& type, bool from_active) { char *ret = NULL; SAVE_PATHS; VarRef vref(this, ref_str, from_active); string val; vtw_type_e t; if (vref.getValue(val, t)) { type = t; // follow original implementation. caller is supposed to free this. ret = strdup(val.c_str()); } RESTORE_PATHS; return ret; } /* set the node corresponding to specified variable ref string to specified * value. * ref_str: var ref string (e.g., "../encrypted-password/@"). * value: value to be set. * to_active: if true, set in "active config". * otherwise in "working config". * return true if successful. otherwise return false. */ bool Cstore::setVarRef(const string& ref_str, const string& value, bool to_active) { /* XXX functions in cli_new only performs "set var ref" operations (e.g., * '$VAR(@) = ""', which sets current node's value to empty string) * during "commit", i.e., if a "set var ref" is specified in * "syntax:", it will not be performed during "set" (but will be * during commit). * * since commit has not been converted to use the new library, it * does not use this function. instead, it uses the "cli_val_engine" * implementation (where filesystem paths are deeply embedded, which * makes it difficult to abstract low-level filesystem operations * from high-level functions). as a result, this function is unused * and untested at the moment. must revisit when converting commit. */ SAVE_PATHS; VarRef vref(this, ref_str, to_active); vector pcomps; bool ret = false; if (vref.getSetPath(pcomps)) { reset_paths(); vtw_def def; if (get_parsed_tmpl(pcomps, false, def)) { if (!def.is_value && !def.tag && !def.multi && def.def_type != ERROR_TYPE) { // currently only support single-value node append_cfg_path(pcomps); if (write_value(value, to_active)) { ret = true; } } } } RESTORE_PATHS; return ret; } /* perform deactivate operation on a node, i.e., make the node * "marked deactivated". * note: assume all validations have been peformed (see activate.cpp). * also, when marking a node as deactivated, all of its descendants * that had been marked deactivated are unmarked. */ bool Cstore::markCfgPathDeactivated(const vector& path_comps) { ASSERT_IN_SESSION; if (cfgPathDeactivated(path_comps)) { output_user("The specified configuration node is already deactivated\n"); // treat as success return true; } SAVE_PATHS; append_cfg_path(path_comps); // note: also mark changed bool ret = (mark_deactivated() && unmark_deactivated_descendants() && mark_changed_with_ancestors()); RESTORE_PATHS; return ret; } /* perform activate operation on a node, i.e., make the node no longer * "marked deactivated". * note: assume all validations have been peformed (see activate.cpp). */ bool Cstore::unmarkCfgPathDeactivated(const vector& path_comps) { ASSERT_IN_SESSION; SAVE_PATHS; append_cfg_path(path_comps); // note: also mark changed bool ret = (unmark_deactivated() && mark_changed_with_ancestors()); RESTORE_PATHS; return ret; } /* "changed" status handling. * the "changed" status is used during commit to check if a node has been * changed. note that if a node is "changed", all of its ancestors are also * considered changed (this follows the original logic). * * the original backend implementation only uses the "changed" marker at * "root" to indicate whether the whole config has changed. for the rest * of the config hierarchy, the original implementation treated all nodes * that are present in the unionfs "changes only" directory as "changed". * * this worked until the introduction of "deactivate". since deactivated * nodes are also present in the "changes only" directory, the backend * treat them as "changed". on the other hand, deleted nodes don't appear * in "changes only", so they are _not_ treated as "changed". this creates * problems in various parts of the backend. * * the new CLI backend/library "marks" all changed nodes explicitly, and the * "changed" status depends on such markers. the marking is done using the * pure virtual mark_changed_with_ancestors() function, which is provided * by the low-level implementation, so it does not have to be done as a * "per-node file marker" as long as the low-level implementation can * correctly answer the "changed" query for a given path. * * note that "changed" nodes does not include "added" and "deleted" nodes. * for the convenience of implementation, the backend must always query * for "changed" nodes *after* "added" and "deleted" nodes. in other * words, the backend will only treat a node as "changed" if it is neither * "added" nor "deleted". currently there are only two places that perform * changed status query: cfgPathGetChildNodesStatus() and * cfgPathGetChildNodesStatusDA(). see those two functions for the usage. * * what this means is that the backend can choose to either mark or not * mark "added"/"deleted" nodes as "changed" at its convenience. for * example, "set" and "delete" always do the marking, but "rename" and * "copy" do not. * * changed status queries are provided by the cfg_node_changed() function, * and changed markers can be removed by unmarkCfgPathChanged() below (used * by "commit"). */ /* unmark "changed" status of specified path in working config. * this is used, e.g., at the end of "commit" to reset a subtree. * note: unmarking a node means all of its descendants are also unmarked, * i.e., they become "unchanged". * return true if successful. otherwise return false. */ bool Cstore::unmarkCfgPathChanged(const vector& path_comps) { ASSERT_IN_SESSION; SAVE_PATHS; append_cfg_path(path_comps); bool ret = unmark_changed_with_descendants(); RESTORE_PATHS; return ret; } ////// protected functions void Cstore::output_user(const char *fmt, ...) { va_list alist; va_start(alist, fmt); if (out_stream) { vfprintf(out_stream, fmt, alist); } else { vprintf(fmt, alist); } va_end(alist); } void Cstore::output_internal(const char *fmt, ...) { va_list alist; va_start(alist, fmt); int fdout = -1; FILE *fout = NULL; do { if ((fdout = open(C_LOGFILE_STDOUT.c_str(), O_WRONLY | O_CREAT, 0660)) == -1) { break; } if (lseek(fdout, 0, SEEK_END) == ((off_t) -1)) { break; } if ((fout = fdopen(fdout, "a")) == NULL) { break; } vfprintf(fout, fmt, alist); } while (0); va_end(alist); if (fout) { fclose(fout); // fdout is implicitly closed } else if (fdout >= 0) { close(fdout); } } ////// private functions bool Cstore::sort_func_deb_version(string a, string b) { return (pkgVersionCompare(a, b) < 0); } void Cstore::sort_nodes(vector& nvec, unsigned int sort_alg) { if (_sort_func_map.find(sort_alg) == _sort_func_map.end()) { return; } sort(nvec.begin(), nvec.end(), _sort_func_map[sort_alg]); } /* try to append the logical path to template path. * is_tag: (output) whether the last component is a "tag". * return false if logical path is not valid. otherwise return true. */ bool Cstore::append_tmpl_path(const vector& path_comps, bool& is_tag) { is_tag = false; for (size_t i = 0; i < path_comps.size(); i++) { push_tmpl_path(path_comps[i]); if (tmpl_node_exists()) { // got exact match. continue to next component. continue; } // not exact match. check if it's a tag. pop_tmpl_path(); push_tmpl_path_tag(); if (tmpl_node_exists()) { // got tag match. continue to next component. if (i == (path_comps.size() - 1)) { // last comp is_tag = true; } continue; } // not a valid path return false; } return true; } /* check whether specified "logical path" is valid template path. * then template at the path is parsed. * path_comps: vector of path components. * validate_vals: whether to validate all "values" along specified path. * def: (output) parsed template. * error: (output) error message if failed. * return false if invalid template path. otherwise return true. * note: * also, if last path component is value (i.e., def.is_value), the template * parsed is actually at "full path - 1". */ bool Cstore::get_parsed_tmpl(const vector& path_comps, bool validate_vals, vtw_def& def, string& error) { // default error message error = "The specified configuration node is not valid"; if (tmpl_path_at_root() && path_comps.size() == 0) { // empty path not valid return false; } /* note: this function may be invoked recursively (depth 1) when * validating values, i.e., validate_value will process variable * reference, which calls this indirectly to get templates. * so need special save/restore identifier. */ const char *not_validating = "get_parsed_tmpl_not_validating"; if (validate_vals) { SAVE_PATHS; } else { save_paths(not_validating); } /* need at least 1 comp to work. 2 comps if last comp is value. * so pop tmpl_path and prepend them. note that path_comps remain * constant. */ vector *pcomps = const_cast *>(&path_comps); vector new_path_comps; if (path_comps.size() < 2) { new_path_comps = path_comps; pcomps = &new_path_comps; for (unsigned int i = 0; i < 2 && pcomps->size() < 2; i++) { if (!tmpl_path_at_root()) { pcomps->insert(pcomps->begin(), pop_tmpl_path()); } } } bool ret = false; do { /* cases for template path: * (1) valid path ending in "actual node", i.e., typeless node, tag node, * single-value node, or multi-value node: * => tmpl at full path. e.g.: * typeless node: "service ssh allow-root" * tag node: "interfaces ethernet" * single-value node: "system gateway-address" * multi-value node: "system name-server" * (2) valid path ending in "value", i.e., tag (value of tag node), or * value of single-/multi-value node: * => tmpl at "full path - 1". e.g.: * "value" of tag node: "interfaces ethernet eth0" * value of single-value node: "system gateway-address 1.1.1.1" * value of multi-value node: "system name-server 2.2.2.2" * (3) invalid path * => no tmpl */ // first scan up to "full path - 1" bool valid = true; for (size_t i = 0; i < (pcomps->size() - 1); i++) { if ((*pcomps)[i] == "") { // only the last component is potentially allowed to be empty str valid = false; break; } bool is_tag; if (append_tmpl_path((*pcomps)[i], is_tag)) { if (is_tag && validate_vals) { /* last comp is tag and want to validate value. * note: validate_val() will use the current tmpl path and cfg path. * so need both at the "node" level before calling it. * at this point cfg path is not pushed yet so no need to * pop it. */ pop_tmpl_path(); if (!validate_val(NULL, (*pcomps)[i])) { // invalid value error = "Value validation failed"; valid = false; break; } // restore tmpl path append_tmpl_path((*pcomps)[i], is_tag); } /* cfg path is not used here but is needed by validate_val(), so * need to keep it in sync with tmpl path. */ push_cfg_path((*pcomps)[i]); } else { // not a valid path valid = false; break; } } if (!valid) { // case (3) break; } /* we are valid up to "full path - 1". now process last path component. * first, if we are case (2), we should find a "value node" at this point. * note: this is only possible if there are more than 1 component. otherwise * we haven't done anything yet. */ if (pcomps->size() > 1) { if (tmpl_parse(def)) { if (def.tag || def.multi || def.def_type != ERROR_TYPE) { // case (2). last component is "value". if (validate_vals) { // validate value if (!validate_val(&def, (*pcomps)[pcomps->size() - 1])) { // invalid value error = "Value validation failed"; break; } } def.is_value = 1; ret = true; break; } } // if no valid template or not a value, it's not case (2) so continue. } // now check last component if ((*pcomps)[pcomps->size() - 1] == "") { // only value is potentially allowed to be empty str break; } push_tmpl_path((*pcomps)[pcomps->size() - 1]); // no need to push cfg path (only needed for validate_val()) if (tmpl_node_exists()) { // case (1). last component is "node". if (!tmpl_parse(def)) { exit_internal("failed to parse tmpl [%s]\n", tmpl_path_to_str().c_str()); } def.is_value = 0; ret = true; break; } // case (3) (fall through) } while (0); if (validate_vals) { RESTORE_PATHS; } else { restore_paths(not_validating); } return ret; } /* check if specified "logical path" is valid for "activate" or * "deactivate" operation. * return true if valid. otherwise return false. */ bool Cstore::validate_act_deact(const vector& path_comps, const string& op, vtw_def& def) { string terr; if (!get_parsed_tmpl(path_comps, false, def, terr)) { output_user("%s\n", terr.c_str()); return false; } { /* XXX this is a temporary workaround for bug 5708, which should be * addressed _after_ the "default value"-related issues have been * resolved (see bug for more details). once those are resolved, * this workaround should be removed and the bug fixed properly. */ if (!def.tag && def.def_type != ERROR_TYPE) { output_user("Cannot %s a leaf configuration node\n", op.c_str()); return false; } } if (def.is_value && !def.tag) { /* last component is a value of a single- or multi-value node (i.e., * a leaf value) => not allowed */ output_user("Cannot %s a leaf configuration value\n", op.c_str()); return false; } if (!cfg_path_exists(path_comps, false, true)) { output_user("Nothing to %s (the specified %s does not exist)\n", op.c_str(), (!def.is_value || def.tag) ? "node" : "value"); return false; } return true; } /* check if specified args is valid for "rename" or "copy" operation. * return true if valid. otherwise return false. */ bool Cstore::validate_rename_copy(const vector& args, const string& op) { if (args.size() != 5 || args[2] != "to") { output_user("Invalid %s command\n", op.c_str()); return false; } string otagnode = args[0]; string otagval = args[1]; string ntagnode = args[3]; string ntagval = args[4]; if (otagnode != ntagnode) { output_user("Cannot %s from \"%s\" to \"%s\"\n", op.c_str(), otagnode.c_str(), ntagnode.c_str()); return false; } // check the old path vector ppath; ppath.push_back(otagnode); ppath.push_back(otagval); vtw_def def; string terr; if (!get_parsed_tmpl(ppath, false, def, terr)) { output_user("%s\n", terr.c_str()); return false; } if (!def.is_value || !def.tag) { // can only rename "tagnode tagvalue" output_user("Cannot %s under \"%s\"\n", op.c_str(), otagnode.c_str()); return false; } if (!cfg_path_exists(ppath, false, true)) { output_user("Configuration \"%s %s\" does not exist\n", otagnode.c_str(), otagval.c_str()); return false; } // check the new path ppath.pop_back(); ppath.push_back(ntagval); if (cfg_path_exists(ppath, false, true)) { output_user("Configuration \"%s %s\" already exists\n", ntagnode.c_str(), ntagval.c_str()); return false; } if (!get_parsed_tmpl(ppath, true, def, terr)) { output_user("%s\n", terr.c_str()); return false; } return true; } // convert args for "move" to be used for equivalent "rename" operation bool Cstore::conv_move_args_for_rename(const vector& args, vector& edit_path_comps, vector& rn_args) { /* note: * "move interfaces ethernet eth2 vif 100 to 200" * is equivalent to * "edit interfaces ethernet eth2" + "rename vif 100 to vif 200" * * set the extra levels and then just validate as rename */ size_t num_args = args.size(); if (num_args < 4) { // need at least 4 args return false; } for (size_t i = 0; i < (num_args - 4); i++) { edit_path_comps.push_back(args[i]); } rn_args.push_back(args[num_args - 4]); // vif rn_args.push_back(args[num_args - 3]); // 100 rn_args.push_back(args[num_args - 2]); // to rn_args.push_back(args[num_args - 4]); // vif rn_args.push_back(args[num_args - 1]); // 200 return true; } /* check if specified "logical path" exists in working config or * active config. * v_def: ptr to parsed template. NULL if none. * return true if it exists. otherwise return false. */ bool Cstore::cfg_path_exists(const vector& path_comps, bool active_cfg, bool include_deactivated) { SAVE_PATHS; append_cfg_path(path_comps); // first check if it's a "node". bool ret = cfg_node_exists(active_cfg); if (!ret) { // doesn't exist as a node. maybe a value? pop_cfg_path(); ret = cfg_value_exists(path_comps[path_comps.size() - 1], active_cfg); } RESTORE_PATHS; if (ret && !include_deactivated && cfgPathDeactivated(path_comps, active_cfg)) { // don't include deactivated ret = false; } return ret; } /* set specified "logical path" in "working config". * output: whether to generate output * return true if successful. otherwise return false. * note: assume specified path is valid (i.e., validateSetPath()). */ bool Cstore::set_cfg_path(const vector& path_comps, bool output) { vector ppath; vtw_def def; bool ret = true; bool path_exists = true; // do the set from the top down SAVE_PATHS; for (size_t i = 0; i < path_comps.size(); i++) { // partial path ppath.push_back(path_comps[i]); // get template at this level if (!get_parsed_tmpl(ppath, false, def)) { output_internal("paths[%s,%s]\n", cfg_path_to_str().c_str(), tmpl_path_to_str().c_str()); for (size_t i = 0; i < ppath.size(); i++) { output_internal(" [%s]\n", ppath[i].c_str()); } exit_internal("failed to get tmpl during set. not validate first?\n"); } // nop if this level already in working (including deactivated) if (cfg_path_exists(ppath, false, true)) { continue; } path_exists = false; // this level not in working. set it. append_cfg_path(ppath); append_tmpl_path(ppath); if (!def.is_value) { // this level is a "node" if (!add_node() || !create_default_children()) { ret = false; break; } } else if (def.tag) { // this level is a "tag value". // add the tag, taking the max tag limit into consideration. if (!add_tag(def) || !create_default_children()) { ret = false; break; } } else { // this level is a "value" of a single-/multi-value node. // go up 1 level to get the node. pop_cfg_path(); if (def.multi) { // value of multi-value node. // add the value, taking the max multi limit into consideration. if (!add_value_to_multi(def, ppath.back())) { ret = false; break; } } else { // value of single-value node if (!write_value(ppath.back())) { ret = false; break; } } } if (!mark_changed_with_ancestors()) { ret = false; break; } RESTORE_PATHS; } RESTORE_PATHS; // if "break" was hit if (ret && def.is_value && def.def_default) { /* a node with default has been explicitly set. needs to be marked * as non-default for display purposes. * * note: when the new value is the same as the old value, the behavior * is different from before, which was a nop. the new behavior is * that the value will remain unchanged, but the "default status" * will be changed, i.e., it will be marked as non-default. */ append_cfg_path(path_comps); pop_cfg_path(); // only do it if it's previously marked default if (marked_display_default(false)) { if ((ret = unmark_display_default())) { /* XXX work around current commit's unionfs implementation problem. * current commit's unionfs implementation looks at the "changes * only" directory (i.e., the r/w portion of the union mount), which * is wrong. * * all config information should be obtained from two directories: * "active" and "working", e.g., instead of looking at whiteout * files in "changes only" to find deleted nodes, nodes that are in * "active" but not in "working" have been deleted. * * in this particular case, commit looks at "changes only" to read * the node.val file. however, since the value didn't change (only * the "default status" changed), node.val doesn't appear in * "changes only". here we re-write the file to force it into * "changes only" so that commit can work correctly. */ vector vvec; read_value_vec(vvec, false); write_value_vec(vvec); // pretend it didn't exist since we changed the status path_exists = false; // also mark changed ret = mark_changed_with_ancestors(); } } RESTORE_PATHS; } if (path_exists) { // whole path already exists if (output) { output_user("The specified configuration node already exists\n"); } // treat as success } return ret; } /* this is the equivalent of the listNodeStatus() from the original * perl API. it provides the "status" ("deleted", "added", "changed", * or "static") of each child node of specified path. * cmap: (output) contains the status of child nodes. * sorted_keys: (output) contains sorted keys. call with NULL if not needed. * * note: this function is NOT "deactivate-aware". */ void Cstore::get_child_nodes_status(const vector& path_comps, Cstore::MapT& cmap, vector *sorted_keys) { // get a union of active and working MapT umap; vector acnodes; vector wcnodes; cfgPathGetChildNodes(path_comps, acnodes, true); cfgPathGetChildNodes(path_comps, wcnodes, false); for (size_t i = 0; i < acnodes.size(); i++) { umap[acnodes[i]] = true; } for (size_t i = 0; i < wcnodes.size(); i++) { umap[wcnodes[i]] = true; } // get the status of each one vector ppath = path_comps; MapT::iterator it = umap.begin(); for (; it != umap.end(); ++it) { string c = (*it).first; ppath.push_back(c); if (sorted_keys) { sorted_keys->push_back(c); } // note: "changed" includes "deleted" and "added", so check those first. if (cfgPathDeleted(ppath)) { cmap[c] = C_NODE_STATUS_DELETED; } else if (cfgPathAdded(ppath)) { cmap[c] = C_NODE_STATUS_ADDED; } else if (cfgPathChanged(ppath)) { cmap[c] = C_NODE_STATUS_CHANGED; } else { cmap[c] = C_NODE_STATUS_STATIC; } ppath.pop_back(); } if (sorted_keys) { sort_nodes(*sorted_keys); } } /* DA version of the above function. * cmap: (output) contains the status of child nodes. * sorted_keys: (output) contains sorted keys. call with NULL if not needed. * * note: this follows the original perl API listNodeStatus() implementation. */ void Cstore::get_child_nodes_status_da(const vector& path_comps, Cstore::MapT& cmap, vector *sorted_keys) { // process deleted nodes first vector del_nodes; cfgPathGetDeletedChildNodesDA(path_comps, del_nodes); for (size_t i = 0; i < del_nodes.size(); i++) { if (sorted_keys) { sorted_keys->push_back(del_nodes[i]); } cmap[del_nodes[i]] = C_NODE_STATUS_DELETED; } // get all nodes in working config vector work_nodes; cfgPathGetChildNodesDA(path_comps, work_nodes, false); vector ppath = path_comps; for (size_t i = 0; i < work_nodes.size(); i++) { ppath.push_back(work_nodes[i]); if (sorted_keys) { sorted_keys->push_back(work_nodes[i]); } /* note: in the DA version here, we do NOT check the deactivate state * when considering the state of the child nodes (which include * deactivated ones). the reason is that this DA function is used * for config output-related operations and should return whether * each node is actually added/deleted from the config independent * of its deactivate state. * * for "added" state, can't use cfgPathAdded() since it's not DA. * * for "changed" state, can't use cfgPathChanged() since it's not DA. * * deleted ones already handled above. */ if (!cfg_path_exists(ppath, true, true) && cfg_path_exists(ppath, false, true)) { cmap[work_nodes[i]] = C_NODE_STATUS_ADDED; } else { SAVE_PATHS; append_cfg_path(ppath); if (cfg_node_changed()) { cmap[work_nodes[i]] = C_NODE_STATUS_CHANGED; } else { cmap[work_nodes[i]] = C_NODE_STATUS_STATIC; } RESTORE_PATHS; } ppath.pop_back(); } if (sorted_keys) { sort_nodes(*sorted_keys); } } /* remove tag at current work path and its subtree. * if specified tag is the last one, also remove the tag node. * return true if successful. otherwise return false. * note: assume current work path is a tag. */ bool Cstore::remove_tag() { if (!remove_node()) { return false; } // go up one level and check if that was the last tag bool ret = true; string c = pop_cfg_path(); vector cnodes; // get child nodes, including deactivated ones. get_all_child_node_names(cnodes, false, true); if (cnodes.size() == 0) { // it was the last tag. remove the node as well. if (!remove_node()) { ret = false; } } push_cfg_path(c); return ret; } /* remove specified value from the multi-value node at current work path. * if specified value is the last one, also remove the multi-value node. * return true if successful. otherwise return false. * note: assume current work path is a multi-value node and specified value is * configured for the node. */ bool Cstore::remove_value_from_multi(const string& value) { // get current values vector vvec; if (!read_value_vec(vvec, false)) { return false; } // remove the value size_t bc = vvec.size(); vector nvec(vvec.begin(), remove(vvec.begin(), vvec.end(), value)); size_t ac = nvec.size(); // sanity check if (ac == bc) { // nothing removed return false; } if (ac == 0) { // was the last value. remove the node. return remove_node(); } else { // write the new values out return write_value_vec(nvec); } } /* check whether specified value exists at current work path. * note: assume current work path is a value node. */ bool Cstore::cfg_value_exists(const string& value, bool active_cfg) { // get current values vector vvec; if (!read_value_vec(vvec, active_cfg)) { return false; } return (find(vvec.begin(), vvec.end(), value) != vvec.end()); } /* validate value at current template path. * def: pointer to parsed template. NULL if none. * val: value to be validated. * return true if valid. otherwise return false. * note: current template and cfg paths both point to the node, * not the value. */ bool Cstore::validate_val(const vtw_def *def, const string& value) { vtw_def ndef; if (!def) { if (!tmpl_parse(ndef)) { exit_internal("failed to parse tmpl [%s]\n", tmpl_path_to_str().c_str()); } def = &ndef; if (def->def_type == ERROR_TYPE) { // not a value node exit_internal("validating non-value node [%s]\n", tmpl_path_to_str().c_str()); } } // validate_value() may change "value". make a copy first. size_t vlen = value.size(); char *vbuf = (char *) malloc(vlen + 1); strncpy(vbuf, value.c_str(), vlen + 1); vbuf[vlen] = 0; bool ret = validate_val_impl((vtw_def *) def, vbuf); free(vbuf); return ret; } /* add tag at current work path. * return true if successful. otherwise return false. * note: assume current work path is a new tag and path from root to parent * already exists. */ bool Cstore::add_tag(const vtw_def& def) { string t = pop_cfg_path(); vector cnodes; // get child nodes, excluding deactivated ones. get_all_child_node_names(cnodes, false, false); bool ret = false; do { if (def.def_tag > 0 && def.def_tag <= cnodes.size()) { // limit exceeded output_user("Cannot set node \"%s\": number of values exceeds limit" "(%d allowed)\n", t.c_str(), def.def_tag); break; } /* XXX the following is the original logic, which is wrong since def_tag * is unsigned. */ if (def.def_tag < 0 && cnodes.size() == 1) { /* XXX special case in the original implementation where the previous * tag should be replaced. this is probably unnecessary since * "rename" can be used for tag node anyway. */ ret = rename_child_node(cnodes[0], t); break; } // neither of the above. just add the tag. ret = add_child_node(t); } while (0); push_cfg_path(t); return ret; } /* add specified value to the multi-value node at current work path. * return true if successful. otherwise return false. * note: assume current work path is a multi-value node and specified value is * not configured for the node. */ bool Cstore::add_value_to_multi(const vtw_def& def, const string& value) { // get current values vector vvec; // ignore return value here. if it failed, vvec is empty. read_value_vec(vvec, false); /* note: XXX the original limit-checking logic uses the same count as tag * node, which is wrong since multi-node values are not stored as * directories in the original implementation. * * also, original logic only applies limit when def_multi > 1. * this was probably to support the special case in the design * when def_multi is 1 to make it behave like a single-value * node (i.e., a subsequent set replaces the value). however, * the implementation uses "-1" as the special case (but def_multi * is unsigned anyway). see also "add_tag()". * * for now just apply the limit for anything >= 1. */ if (def.def_multi >= 1 && vvec.size() >= def.def_multi) { // limit exceeded output_user("Cannot set value \"%s\": number of values exceeded " "(%d allowed)\n", value.c_str(), def.def_multi); return false; } // append the value vvec.push_back(value); return write_value_vec(vvec); } /* this uses the get_all_child_node_names_impl() from the underlying * implementation but provides the option to exclude deactivated nodes. */ void Cstore::get_all_child_node_names(vector& cnodes, bool active_cfg, bool include_deactivated) { vector nodes; get_all_child_node_names_impl(nodes, active_cfg); for (size_t i = 0; i < nodes.size(); i++) { if (!include_deactivated) { push_cfg_path(nodes[i]); bool skip = marked_deactivated(active_cfg); pop_cfg_path(); if (skip) { continue; } } cnodes.push_back(nodes[i]); } } /* create all child nodes of current work path that have default values * return true if successful. otherwise return false. * note: assume current work path has just been created so no child * nodes exist. */ bool Cstore::create_default_children() { vector tcnodes; get_all_tmpl_child_node_names(tcnodes); bool ret = true; for (size_t i = 0; i < tcnodes.size(); i++) { push_tmpl_path(tcnodes[i]); vtw_def def; if (tmpl_node_exists() && tmpl_parse(def)) { if (def.def_default) { // has default value. set it. push_cfg_path(tcnodes[i]); if (!add_node() || !write_value(def.def_default) || !mark_display_default()) { ret = false; } pop_cfg_path(); } } pop_tmpl_path(); if (!ret) { break; } } return ret; } /* return environment string for "edit"-related operations based on current * work/tmpl paths. */ void Cstore::get_edit_env(string& env) { vector lvec; get_edit_level(lvec); string lvl; for (size_t i = 0; i < lvec.size(); i++) { if (lvl.length() > 0) { lvl += " "; } lvl += lvec[i]; } env = ("export " + C_ENV_EDIT_LEVEL + "='" + get_edit_level_path() + "';"); env += (" export " + C_ENV_TMPL_LEVEL + "='" + get_tmpl_level_path() + "';"); env += (" export " + C_ENV_SHELL_PROMPT + "='" + get_shell_prompt(lvl) + "';"); } // return shell prompt string for specified edit level string Cstore::get_shell_prompt(const string& level) { string lvl = level; if (lvl.length() > 0) { lvl = " " + lvl; } return ("[edit" + lvl + "]\\n\\u@\\h# "); } // escape the single quotes in the string for shell void Cstore::shell_escape_squotes(string& str) { size_t sq = 0; while ((sq = str.find('\'', sq)) != str.npos) { str.replace(sq, 1, "'\\''"); sq += 4; } }