summaryrefslogtreecommitdiff
path: root/src/cstore/unionfs/cstore-unionfs.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/cstore/unionfs/cstore-unionfs.cpp')
-rw-r--r--src/cstore/unionfs/cstore-unionfs.cpp1078
1 files changed, 1078 insertions, 0 deletions
diff --git a/src/cstore/unionfs/cstore-unionfs.cpp b/src/cstore/unionfs/cstore-unionfs.cpp
new file mode 100644
index 0000000..977a597
--- /dev/null
+++ b/src/cstore/unionfs/cstore-unionfs.cpp
@@ -0,0 +1,1078 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <map>
+#include <fstream>
+#include <sstream>
+
+#include <errno.h>
+#include <sys/mount.h>
+
+extern "C" {
+#include "cli_val.h"
+#include "cli_objects.h"
+}
+
+#include "cstore-unionfs.hpp"
+
+
+////// constants
+// environment vars defining root dirs
+const string UnionfsCstore::C_ENV_TMPL_ROOT = "VYATTA_CONFIG_TEMPLATE";
+const string UnionfsCstore::C_ENV_WORK_ROOT = "VYATTA_TEMP_CONFIG_DIR";
+const string UnionfsCstore::C_ENV_ACTIVE_ROOT
+ = "VYATTA_ACTIVE_CONFIGURATION_DIR";
+const string UnionfsCstore::C_ENV_CHANGE_ROOT = "VYATTA_CHANGES_ONLY_DIR";
+const string UnionfsCstore::C_ENV_TMP_ROOT = "VYATTA_CONFIG_TMP";
+
+// default root dirs/paths
+const string UnionfsCstore::C_DEF_TMPL_ROOT
+ = "/opt/vyatta/share/vyatta-cfg/templates";
+const string UnionfsCstore::C_DEF_CFG_ROOT
+ = "/opt/vyatta/config";
+const string UnionfsCstore::C_DEF_ACTIVE_ROOT
+ = UnionfsCstore::C_DEF_CFG_ROOT + "/active";
+const string UnionfsCstore::C_DEF_CHANGE_PREFIX = "/tmp/changes_only_";
+const string UnionfsCstore::C_DEF_WORK_PREFIX
+ = UnionfsCstore::C_DEF_CFG_ROOT + "/tmp/new_config_";
+const string UnionfsCstore::C_DEF_TMP_PREFIX
+ = UnionfsCstore::C_DEF_CFG_ROOT + "/tmp/tmp_";
+
+// markers
+const string UnionfsCstore::C_MARKER_DEF_VALUE = "def";
+const string UnionfsCstore::C_MARKER_DEACTIVATE = ".disable";
+const string UnionfsCstore::C_MARKER_CHANGED = ".modified";
+const string UnionfsCstore::C_MARKER_UNSAVED = ".unsaved";
+const string UnionfsCstore::C_COMMITTED_MARKER_FILE = "/tmp/.changes";
+const string UnionfsCstore::C_COMMENT_FILE = ".comment";
+
+
+////// static
+static map<char, string> _fs_escape_chars;
+static map<string, char> _fs_unescape_chars;
+static void
+_init_fs_escape_chars()
+{
+ _fs_escape_chars[-1] = "\%\%\%";
+ _fs_escape_chars['%'] = "\%25";
+ _fs_escape_chars['/'] = "\%2F";
+
+ _fs_unescape_chars["\%\%\%"] = -1;
+ _fs_unescape_chars["\%25"] = '%';
+ _fs_unescape_chars["\%2F"] = '/';
+}
+
+static string
+_escape_char(char c)
+{
+ map<char, string>::iterator p = _fs_escape_chars.find(c);
+ if (p != _fs_escape_chars.end()) {
+ return _fs_escape_chars[c];
+ } else {
+ return string(1, c);
+ }
+}
+
+static map<string, string> _escape_path_name_cache;
+
+static string
+_escape_path_name(const string& path)
+{
+ map<string, string>::iterator p = _escape_path_name_cache.find(path);
+ if (p != _escape_path_name_cache.end()) {
+ // found escaped string in cache. just return it.
+ return _escape_path_name_cache[path];
+ }
+
+ // special case for empty string
+ string npath = (path.size() == 0) ? _fs_escape_chars[-1] : "";
+ for (unsigned int i = 0; i < path.size(); i++) {
+ npath += _escape_char(path[i]);
+ }
+
+ // cache it before return
+ _escape_path_name_cache[path] = npath;
+ return npath;
+}
+
+static map<string, string> _unescape_path_name_cache;
+
+static string
+_unescape_path_name(const string& path)
+{
+ map<string, string>::iterator p = _unescape_path_name_cache.find(path);
+ if (p != _unescape_path_name_cache.end()) {
+ // found unescaped string in cache. just return it.
+ return _unescape_path_name_cache[path];
+ }
+
+ // assume all escape patterns are 3-char
+ string npath = "";
+ for (unsigned int i = 0; i < path.size(); i++) {
+ if ((path.size() - i) < 3) {
+ npath += path.substr(i);
+ break;
+ }
+ string s = path.substr(i, 3);
+ map<string, char>::iterator p = _fs_unescape_chars.find(s);
+ if (p != _fs_unescape_chars.end()) {
+ char c = _fs_unescape_chars[s];
+ if (path.size() == 3 && c == -1) {
+ // special case for empty string
+ npath = "";
+ break;
+ }
+ npath += string(1, _fs_unescape_chars[s]);
+ // skip the escape sequence
+ i += 2;
+ } else {
+ npath += path.substr(i, 1);
+ }
+ }
+ // cache it before return
+ _unescape_path_name_cache[path] = npath;
+ return npath;
+}
+
+
+////// constructor/destructor
+/* "current session" constructor.
+ * this constructor sets up the object from environment.
+ * used when environment is already set up, i.e., when operating on the
+ * "current" config session. e.g., in the following scenarios
+ * configure commands
+ * perl module
+ * shell "current session" api
+ *
+ * note: this also applies when using the cstore in operational mode,
+ * in which case only the template root and the active root will be
+ * valid.
+ */
+UnionfsCstore::UnionfsCstore(bool use_edit_level)
+{
+ // set up root dir strings
+ char *val;
+ if ((val = getenv(C_ENV_TMPL_ROOT.c_str()))) {
+ tmpl_path = val;
+ } else {
+ tmpl_path = C_DEF_TMPL_ROOT;
+ }
+ tmpl_root = tmpl_path; // save a copy of tmpl root
+ if ((val = getenv(C_ENV_WORK_ROOT.c_str()))) {
+ work_root = val;
+ }
+ if ((val = getenv(C_ENV_TMP_ROOT.c_str()))) {
+ tmp_root = val;
+ }
+ if ((val = getenv(C_ENV_ACTIVE_ROOT.c_str()))) {
+ active_root = val;
+ } else {
+ active_root = C_DEF_ACTIVE_ROOT;
+ }
+ if ((val = getenv(C_ENV_CHANGE_ROOT.c_str()))) {
+ change_root = val;
+ }
+
+ /* note: the original perl API module does not use the edit levels
+ * from environment. only the actual CLI operations use them.
+ * so here make it an option.
+ */
+ if (use_edit_level) {
+ // set up path strings
+ if ((val = getenv(C_ENV_EDIT_LEVEL.c_str()))) {
+ mutable_cfg_path = val;
+ }
+ if ((val = getenv(C_ENV_TMPL_LEVEL.c_str()))) {
+ tmpl_path /= val;
+ }
+ }
+ _init_fs_escape_chars();
+}
+
+/* "specific session" constructor.
+ * this constructor sets up the object for the specified session ID and
+ * returns an environment string that can be "evaled" to set up the
+ * shell environment.
+ *
+ * used when the session environment needs to be established. this is
+ * mainly for the shell functions that set up configuration sessions.
+ * i.e., the "vyatta-cfg-cmd-wrapper" (on boot or for GUI etc.) and
+ * the cfg completion script (when entering configure mode).
+ *
+ * sid: session ID.
+ * env: (output) environment string.
+ *
+ * note: this does NOT set up the session. caller needs to use the
+ * explicit session setup/teardown functions as needed.
+ */
+UnionfsCstore::UnionfsCstore(const string& sid, string& env)
+ : Cstore(env)
+{
+ tmpl_root = C_DEF_TMPL_ROOT;
+ tmpl_path = tmpl_root;
+ active_root = C_DEF_ACTIVE_ROOT;
+ work_root = (C_DEF_WORK_PREFIX + sid);
+ change_root = (C_DEF_CHANGE_PREFIX + sid);
+ tmp_root = (C_DEF_TMP_PREFIX + sid);
+
+ string declr = " declare -x -r "; // readonly vars
+ env += " umask 002; {";
+ env += (declr + C_ENV_ACTIVE_ROOT + "=" + active_root.file_string());
+ env += (declr + C_ENV_CHANGE_ROOT + "=" + change_root.file_string() + ";");
+ env += (declr + C_ENV_WORK_ROOT + "=" + work_root.file_string() + ";");
+ env += (declr + C_ENV_TMP_ROOT + "=" + tmp_root.file_string() + ";");
+ env += (declr + C_ENV_TMPL_ROOT + "=" + tmpl_root.file_string() + ";");
+ env += " } >&/dev/null || true";
+
+ // set up path strings using level vars
+ char *val;
+ if ((val = getenv(C_ENV_EDIT_LEVEL.c_str()))) {
+ mutable_cfg_path = val;
+ }
+ if ((val = getenv(C_ENV_TMPL_LEVEL.c_str()))) {
+ tmpl_path /= val;
+ }
+
+ _init_fs_escape_chars();
+}
+
+UnionfsCstore::~UnionfsCstore()
+{
+}
+
+
+////// public virtual functions declared in base class
+bool
+UnionfsCstore::markSessionUnsaved()
+{
+ b_fs::path marker = work_root / C_MARKER_UNSAVED;
+ if (b_fs::exists(marker)) {
+ // already marked. treat as success.
+ return true;
+ }
+ if (!create_file(marker.file_string())) {
+ output_internal("failed to mark unsaved [%s]\n",
+ marker.file_string().c_str());
+ return false;
+ }
+ return true;
+}
+
+bool
+UnionfsCstore::unmarkSessionUnsaved()
+{
+ b_fs::path marker = work_root / C_MARKER_UNSAVED;
+ if (!b_fs::exists(marker)) {
+ // not marked. treat as success.
+ return true;
+ }
+ try {
+ b_fs::remove(marker);
+ } catch (...) {
+ output_internal("failed to unmark unsaved [%s]\n",
+ marker.file_string().c_str());
+ return false;
+ }
+ return true;
+}
+
+bool
+UnionfsCstore::sessionUnsaved()
+{
+ b_fs::path marker = work_root / C_MARKER_UNSAVED;
+ return b_fs::exists(marker);
+}
+
+bool
+UnionfsCstore::sessionChanged()
+{
+ b_fs::path marker = work_root / C_MARKER_CHANGED;
+ return b_fs::exists(marker);
+}
+
+/* set up the session associated with this object.
+ * the session comes from either the environment or the session ID
+ * (see the two different constructors).
+ */
+bool
+UnionfsCstore::setupSession()
+{
+ if (!b_fs::exists(work_root)) {
+ // session doesn't exist. create dirs.
+ try {
+ b_fs::create_directories(work_root);
+ b_fs::create_directories(change_root);
+ b_fs::create_directories(tmp_root);
+ if (!b_fs::exists(active_root)) {
+ // this should only be needed on boot
+ b_fs::create_directories(active_root);
+ }
+ } catch (...) {
+ output_internal("setup session failed to create session directories\n");
+ return false;
+ }
+
+ // union mount
+ string mopts = ("dirs=" + change_root.file_string() + "=rw:"
+ + active_root.file_string() + "=ro");
+ if (mount("unionfs", work_root.file_string().c_str(), "unionfs", 0,
+ mopts.c_str()) != 0) {
+ output_internal("setup session mount failed [%s][%s]\n",
+ strerror(errno), work_root.file_string().c_str());
+ return false;
+ }
+ } else if (!b_fs::is_directory(work_root)) {
+ output_internal("setup session not dir [%s]\n",
+ work_root.file_string().c_str());
+ return false;
+ }
+ return true;
+}
+
+/* tear down the session associated with this object.
+ * the session comes from either the environment or the session ID
+ * (see the two different constructors).
+ */
+bool
+UnionfsCstore::teardownSession()
+{
+ // check if session exists
+ string wstr = work_root.file_string();
+ if (wstr.empty() || wstr.find(C_DEF_WORK_PREFIX) != 0
+ || !b_fs::exists(work_root) || !b_fs::is_directory(work_root)) {
+ // no session
+ output_internal("teardown invalid session [%s]\n", wstr.c_str());
+ return false;
+ }
+
+ // unmount the work root (union)
+ if (umount(wstr.c_str()) != 0) {
+ output_internal("teardown session umount failed [%s][%s]\n",
+ strerror(errno), wstr.c_str());
+ return false;
+ }
+
+ // remove session directories
+ bool ret = false;
+ try {
+ if (b_fs::remove_all(work_root) != 0
+ && b_fs::remove_all(change_root) != 0
+ && b_fs::remove_all(tmp_root) != 0) {
+ ret = true;
+ }
+ } catch (...) {
+ }
+ if (!ret) {
+ output_internal("failed to remove session directories\n");
+ }
+ return ret;
+}
+
+/* whether an actual config session is associated with this object.
+ * the session comes from either the environment or the session ID
+ * (see the two different constructors).
+ */
+bool
+UnionfsCstore::inSession()
+{
+ string wstr = work_root.file_string();
+ return (!wstr.empty() && wstr.find(C_DEF_WORK_PREFIX) == 0
+ && b_fs::exists(work_root) && b_fs::is_directory(work_root));
+}
+
+
+////// virtual functions defined in base class
+/* check if current tmpl_path is a valid tmpl dir.
+ * return true if valid. otherwise return false.
+ */
+bool
+UnionfsCstore::tmpl_node_exists()
+{
+ return (b_fs::exists(tmpl_path) && b_fs::is_directory(tmpl_path));
+}
+
+/* parse template at current tmpl_path.
+ * def: for storing parsed template.
+ * return true if successful. otherwise return false.
+ */
+bool
+UnionfsCstore::tmpl_parse(vtw_def& def)
+{
+ push_tmpl_path(DEF_NAME);
+ bool ret = (b_fs::exists(tmpl_path) && b_fs::is_regular(tmpl_path)
+ && parse_def(&def, tmpl_path.file_string().c_str(), FALSE) == 0);
+ pop_tmpl_path();
+ return ret;
+}
+
+bool
+UnionfsCstore::cfg_node_exists(bool active_cfg)
+{
+ b_fs::path p = (active_cfg ? get_active_path() : get_work_path());
+ return (b_fs::exists(p) && b_fs::is_directory(p));
+}
+
+bool
+UnionfsCstore::add_node()
+{
+ bool ret = true;
+ try {
+ if (!b_fs::create_directory(get_work_path())) {
+ // already exists. shouldn't call this function.
+ ret = false;
+ }
+ } catch (...) {
+ ret = false;
+ }
+ if (!ret) {
+ output_internal("failed to add node [%s]\n",
+ get_work_path().file_string().c_str());
+ }
+ return ret;
+}
+
+bool
+UnionfsCstore::remove_node()
+{
+ if (!b_fs::exists(get_work_path()) || !b_fs::is_directory(get_work_path())) {
+ output_internal("remove non-existent node [%s]\n",
+ get_work_path().file_string().c_str());
+ return false;
+ }
+ bool ret = false;
+ try {
+ if (b_fs::remove_all(get_work_path()) != 0) {
+ ret = true;
+ }
+ } catch (...) {
+ ret = false;
+ }
+ if (!ret) {
+ output_internal("failed to remove node [%s]\n",
+ get_work_path().file_string().c_str());
+ }
+ return ret;
+}
+
+void
+UnionfsCstore::get_all_child_node_names_impl(vector<string>& cnodes,
+ bool active_cfg)
+{
+ b_fs::path p = (active_cfg ? get_active_path() : get_work_path());
+ get_all_child_dir_names(p, cnodes);
+
+ /* XXX special cases to emulate original perl API behavior.
+ * original perl listNodes() and listOrigNodes() return everything
+ * under a node (except for ".*"), including "node.val" and "def".
+ *
+ * perl API should operate at abstract level and should not access
+ * such implementation-specific details. however, currently
+ * things like config output depend on this behavior, so this
+ * function needs to return them for now.
+ *
+ * use a whilelist-approach, i.e., only add the following:
+ * node.val
+ * def
+ */
+ if (b_fs::exists(p / VAL_NAME) && b_fs::is_regular(p / VAL_NAME)) {
+ cnodes.push_back(VAL_NAME);
+ }
+ if (b_fs::exists(p / C_MARKER_DEF_VALUE)
+ && b_fs::is_regular(p / C_MARKER_DEF_VALUE)) {
+ cnodes.push_back(C_MARKER_DEF_VALUE);
+ }
+}
+
+bool
+UnionfsCstore::read_value_vec(vector<string>& vvec, bool active_cfg)
+{
+ push_cfg_path_val();
+ b_fs::path vpath = (active_cfg ? get_active_path() : get_work_path());
+ bool ret = false;
+ do {
+ string ostr;
+ if (!read_whole_file(vpath, ostr)) {
+ break;
+ }
+
+ /* XXX original implementation used to remove a trailing '\n' after
+ * a read. it was only necessary because it was adding a '\n' when
+ * writing the file. don't remove anything now since we shouldn't
+ * be writing it any more.
+ */
+ // separate values using newline as delimiter
+ unsigned int start_idx = 0, idx = 0;
+ for (; idx < ostr.size(); idx++) {
+ if (ostr[idx] == '\n') {
+ // got a value
+ vvec.push_back(ostr.substr(start_idx, (idx - start_idx)));
+ start_idx = idx + 1;
+ }
+ }
+ if (start_idx < ostr.size()) {
+ vvec.push_back(ostr.substr(start_idx, (idx - start_idx)));
+ } else {
+ // last char is a newline => another empty value
+ vvec.push_back("");
+ }
+ ret = true;
+ } while (0);
+ pop_cfg_path();
+ return ret;
+}
+
+bool
+UnionfsCstore::write_value_vec(const vector<string>& vvec, bool active_cfg)
+{
+ push_cfg_path_val();
+ bool ret = false;
+ b_fs::path wp = (active_cfg ? get_active_path() : get_work_path());
+ do {
+ if (b_fs::exists(wp) && !b_fs::is_regular(wp)) {
+ // not a file
+ break;
+ }
+
+ string ostr = "";
+ for (unsigned int i = 0; i < vvec.size(); i++) {
+ if (i > 0) {
+ // subsequent values require delimiter
+ ostr += "\n";
+ }
+ ostr += vvec[i];
+ }
+
+ if (!write_file(wp.file_string().c_str(), ostr)) {
+ break;
+ }
+ ret = true;
+ } while (0);
+ pop_cfg_path();
+ if (!ret) {
+ output_internal("failed to write node value [%s]\n",
+ wp.file_string().c_str());
+ }
+ return ret;
+}
+
+bool
+UnionfsCstore::rename_child_node(const string& oname, const string& nname)
+{
+ b_fs::path opath = get_work_path() / oname;
+ b_fs::path npath = get_work_path() / nname;
+ if (!b_fs::exists(opath) || !b_fs::is_directory(opath)
+ || b_fs::exists(npath)) {
+ output_internal("cannot rename node [%s,%s,%s]\n",
+ get_work_path().file_string().c_str(),
+ oname.c_str(), nname.c_str());
+ return false;
+ }
+ bool ret = true;
+ try {
+ /* somehow b_fs::rename() can't be used here as it considers the operation
+ * "Invalid cross-device link" and fails with an exception, probably due
+ * to unionfs in some way.
+ * do it the hard way.
+ */
+ recursive_copy_dir(opath, npath);
+ if (b_fs::remove_all(opath) == 0) {
+ ret = false;
+ }
+ } catch (...) {
+ ret = false;
+ }
+ if (!ret) {
+ output_internal("failed to rename node [%s,%s]\n",
+ opath.file_string().c_str(),
+ npath.file_string().c_str());
+ }
+ return ret;
+}
+
+bool
+UnionfsCstore::copy_child_node(const string& oname, const string& nname)
+{
+ b_fs::path opath = get_work_path() / oname;
+ b_fs::path npath = get_work_path() / nname;
+ if (!b_fs::exists(opath) || !b_fs::is_directory(opath)
+ || b_fs::exists(npath)) {
+ output_internal("cannot copy node [%s,%s,%s]\n",
+ get_work_path().file_string().c_str(),
+ oname.c_str(), nname.c_str());
+ return false;
+ }
+ try {
+ recursive_copy_dir(opath, npath);
+ } catch (...) {
+ output_internal("failed to copy node [%s,%s,%s]\n",
+ get_work_path().file_string().c_str(),
+ oname.c_str(), nname.c_str());
+ return false;
+ }
+ return true;
+}
+
+bool
+UnionfsCstore::mark_display_default()
+{
+ b_fs::path marker = get_work_path() / C_MARKER_DEF_VALUE;
+ if (b_fs::exists(marker)) {
+ // already marked. treat as success.
+ return true;
+ }
+ if (!create_file(marker.file_string())) {
+ output_internal("failed to mark default [%s]\n",
+ get_work_path().file_string().c_str());
+ return false;
+ }
+ return true;
+}
+
+bool
+UnionfsCstore::unmark_display_default()
+{
+ b_fs::path marker = get_work_path() / C_MARKER_DEF_VALUE;
+ if (!b_fs::exists(marker)) {
+ // not marked. treat as success.
+ return true;
+ }
+ try {
+ b_fs::remove(marker);
+ } catch (...) {
+ output_internal("failed to unmark default [%s]\n",
+ get_work_path().file_string().c_str());
+ return false;
+ }
+ return true;
+}
+
+bool
+UnionfsCstore::marked_display_default()
+{
+ b_fs::path marker = get_work_path() / C_MARKER_DEF_VALUE;
+ return b_fs::exists(marker);
+}
+
+bool
+UnionfsCstore::marked_deactivated(bool active_cfg)
+{
+ b_fs::path p = (active_cfg ? get_active_path() : get_work_path());
+ b_fs::path marker = p / C_MARKER_DEACTIVATE;
+ return b_fs::exists(marker);
+}
+
+bool
+UnionfsCstore::mark_deactivated()
+{
+ b_fs::path marker = get_work_path() / C_MARKER_DEACTIVATE;
+ if (b_fs::exists(marker)) {
+ // already marked. treat as success.
+ return true;
+ }
+ if (!create_file(marker.file_string())) {
+ output_internal("failed to mark deactivated [%s]\n",
+ get_work_path().file_string().c_str());
+ return false;
+ }
+ return true;
+}
+
+bool
+UnionfsCstore::unmark_deactivated()
+{
+ b_fs::path marker = get_work_path() / C_MARKER_DEACTIVATE;
+ if (!b_fs::exists(marker)) {
+ // not deactivated. treat as success.
+ return true;
+ }
+ try {
+ b_fs::remove(marker);
+ } catch (...) {
+ output_internal("failed to unmark deactivated [%s]\n",
+ get_work_path().file_string().c_str());
+ return false;
+ }
+ return true;
+}
+
+bool
+UnionfsCstore::unmark_deactivated_descendants()
+{
+ bool ret = false;
+ do {
+ // sanity check
+ if (!b_fs::is_directory(get_work_path())) {
+ break;
+ }
+
+ vector<b_fs::path> markers;
+ b_fs::recursive_directory_iterator di(get_work_path());
+ for (; di != b_fs::recursive_directory_iterator(); ++di) {
+ if (!b_fs::is_regular(di->path())
+ || di->path().filename() != C_MARKER_DEACTIVATE) {
+ // not marker
+ continue;
+ }
+ if (di->path().parent_path() == get_work_path()) {
+ // don't unmark the node itself
+ continue;
+ }
+ markers.push_back(di->path());
+ }
+ try {
+ for (unsigned int i = 0; i < markers.size(); i++) {
+ b_fs::remove(markers[i]);
+ }
+ } catch (...) {
+ break;
+ }
+ ret = true;
+ } while (0);
+ if (!ret) {
+ output_internal("failed to unmark deactivated descendants [%s]\n",
+ get_work_path().file_string().c_str());
+ }
+ return ret;
+}
+
+bool
+UnionfsCstore::mark_changed()
+{
+ if (!mutable_cfg_path.has_parent_path()) {
+ /* at root, mark changed. root marker is needed by the original
+ * implementation as an indication of whether the whole config
+ * has changed.
+ */
+ b_fs::path marker = get_work_path() / C_MARKER_CHANGED;
+ if (b_fs::exists(marker)) {
+ // already marked. treat as success.
+ return true;
+ }
+ if (!create_file(marker.file_string())) {
+ output_internal("failed to mark changed [%s]\n",
+ get_work_path().file_string().c_str());
+ return false;
+ }
+ return true;
+ }
+
+ /* XXX not at root => nop for now.
+ * we should be marking changed here. however, as commit is still
+ * using its own unionfs implementation, it will not understand the
+ * markers and therefore will not perform the necessary cleanup when
+ * it's done.
+ *
+ * for now, don't mark anything besides root. the query function
+ * will use unionfs-specific implementation (changes-only dir).
+ */
+ return true;
+}
+
+// remove the comment at the current work path
+bool
+UnionfsCstore::remove_comment()
+{
+ b_fs::path cfile = get_work_path() / C_COMMENT_FILE;
+ if (!b_fs::exists(cfile)) {
+ return false;
+ }
+ try {
+ b_fs::remove(cfile);
+ } catch (...) {
+ output_internal("failed to remove comment [%s]\n",
+ cfile.file_string().c_str());
+ return false;
+ }
+ return true;
+}
+
+// set comment at the current work path
+bool
+UnionfsCstore::set_comment(const string& comment)
+{
+ b_fs::path cfile = get_work_path() / C_COMMENT_FILE;
+ return write_file(cfile.file_string(), comment);
+}
+
+// discard all changes in working config
+bool
+UnionfsCstore::discard_changes(unsigned long long& num_removed)
+{
+ // unionfs-specific implementation
+ vector<b_fs::path> files;
+ vector<b_fs::path> directories;
+ // iterate through all entries in change root
+ b_fs::directory_iterator di(change_root);
+ for (; di != b_fs::directory_iterator(); ++di) {
+ if (b_fs::is_directory(di->path())) {
+ directories.push_back(di->path());
+ } else {
+ files.push_back(di->path());
+ }
+ }
+
+ // remove and count
+ try {
+ num_removed = 0;
+ for (unsigned int i = 0; i < files.size(); i++) {
+ b_fs::remove(files[i]);
+ num_removed++;
+ }
+ for (unsigned int i = 0; i < directories.size(); i++) {
+ num_removed += b_fs::remove_all(directories[i]);
+ }
+ } catch (...) {
+ output_internal("discard failed [%s]\n",
+ change_root.file_string().c_str());
+ return false;
+ }
+ return true;
+}
+
+// get comment at the current work or active path
+bool
+UnionfsCstore::get_comment(string& comment, bool active_cfg)
+{
+ b_fs::path cfile = (active_cfg ? get_active_path() : get_work_path());
+ cfile /= C_COMMENT_FILE;
+ return read_whole_file(cfile, comment);
+}
+
+bool
+UnionfsCstore::marked_changed()
+{
+ /* this function is only called by cfgPathChanged() in base class.
+ *
+ * XXX currently just use the changes_only dir for this query.
+ * see explanation in mark_changed().
+ *
+ * this implementation relies on the fact that cfgPathChanged()
+ * includes deleted/added nodes (including deactivated/activated
+ * nodes since it's NOT deactivate-aware). if that is not the case,
+ * result will be different between deleted nodes (NOT IN
+ * changes_only) and deactivated nodes (IN changes_only).
+ */
+ return b_fs::exists(get_change_path());
+}
+
+/* XXX currently "committed marking" is done inside commit.
+ * TODO move "committed marking" out of commit and into low-level
+ * implementation (here).
+ */
+/* return whether current "cfg path" has been committed, i.e., whether
+ * the set or delete operation on the path has been processed by commit.
+ * is_set: whether the operation is set (for sanity check as there can
+ * be only one operation on the path).
+ */
+bool
+UnionfsCstore::marked_committed(const vtw_def& def, bool is_set)
+{
+ b_fs::path cpath = mutable_cfg_path;
+ string com_str = cpath.file_string() + "/";
+ if (def.is_value && !def.tag) {
+ // path includes leaf value. construct the right string.
+ string val = _unescape_path_name(cpath.filename());
+ cpath = cpath.parent_path();
+ /* XXX current commit implementation escapes value strings for
+ * single-value nodes but not for multi-value nodes for some
+ * reason. the following match current behavior.
+ */
+ if (!def.multi) {
+ val = _escape_path_name(val);
+ }
+ com_str = cpath.file_string() + "/value:" + val;
+ }
+ com_str = (is_set ? "+ " : "- ") + com_str;
+ return committed_marker_exists(com_str);
+}
+
+bool
+UnionfsCstore::validate_val_impl(vtw_def *def, char *value)
+{
+ /* XXX filesystem paths/accesses are completely embedded in var ref lib.
+ * for now, treat the lib as a unionfs-specific implementation.
+ * generalizing it will need a rewrite.
+ * set the handle to be used during validate_value() for var ref
+ * processing. this is a global var in cli_new.c.
+ */
+ var_ref_handle = (void *) this;
+ bool ret = validate_value(def, value);
+ var_ref_handle = NULL;
+ return ret;
+}
+
+void
+UnionfsCstore::get_edit_level(vector<string>& pcomps) {
+ b_fs::path opath = mutable_cfg_path; // use a copy
+ while (opath.has_parent_path()) {
+ pcomps.insert(pcomps.begin(), pop_path(opath));
+ }
+}
+
+string
+UnionfsCstore::cfg_path_to_str() {
+ string cpath = mutable_cfg_path.file_string();
+ if (cpath.length() == 0) {
+ cpath = "/";
+ }
+ return cpath;
+}
+
+string
+UnionfsCstore::tmpl_path_to_str() {
+ // return only the mutable part
+ string tpath = tmpl_path.file_string();
+ tpath.erase(0, tmpl_root.file_string().length());
+ if (tpath.length() == 0) {
+ tpath = "/";
+ }
+ return tpath;
+}
+
+
+////// private functions
+void
+UnionfsCstore::push_path(b_fs::path& old_path, const string& new_comp)
+{
+ string comp = _escape_path_name(new_comp);
+ old_path /= comp;
+}
+
+string
+UnionfsCstore::pop_path(b_fs::path& path)
+{
+ string ret = _unescape_path_name(path.filename());
+ /* note: contrary to documentation, remove_filename() does not remove
+ * trailing slash.
+ */
+ path = path.parent_path();
+ return ret;
+}
+
+void
+UnionfsCstore::get_all_child_dir_names(b_fs::path root, vector<string>& nodes)
+{
+ if (!b_fs::exists(root) || !b_fs::is_directory(root)) {
+ // not a valid root. nop.
+ return;
+ }
+ b_fs::directory_iterator di(root);
+ for (; di != b_fs::directory_iterator(); ++di) {
+ // must be directory
+ if (!b_fs::is_directory(di->path())) {
+ continue;
+ }
+ // name cannot start with "."
+ if (di->path().file_string().substr(0, 1) == ".") {
+ continue;
+ }
+ // found one
+ nodes.push_back(_unescape_path_name(di->path().filename()));
+ }
+}
+
+bool
+UnionfsCstore::write_file(const string& file, const string& data)
+{
+ try {
+ // make sure the path exists
+ b_fs::path fpath(file);
+ b_fs::create_directories(fpath.parent_path());
+
+ // write the file
+ ofstream fout;
+ fout.exceptions(ofstream::failbit | ofstream::badbit);
+ fout.open(file.c_str(), ios_base::out | ios_base::trunc);
+ fout << data;
+ fout.close();
+ } catch (...) {
+ return false;
+ }
+ return true;
+}
+
+bool
+UnionfsCstore::read_whole_file(const b_fs::path& fpath, string& data)
+{
+ /* must exist, be a regular file, and smaller than limit (we're going
+ * to read the whole thing).
+ */
+ if (!b_fs::exists(fpath) || !b_fs::is_regular(fpath)
+ || b_fs::file_size(fpath) > MAX_FILE_READ_SIZE) {
+ return false;
+ }
+ stringbuf sbuf;
+ ifstream fin(fpath.file_string().c_str());
+ fin >> &sbuf;
+ fin.close();
+ /* note: if file contains just a newline => (eof() && fail())
+ * so only checking bad() and eof() (we want whole file).
+ */
+ if (fin.bad() || !fin.eof()) {
+ // read failed
+ return false;
+ }
+ data = sbuf.str();
+ return true;
+}
+
+/* return whether specified "commited marker" exists in the
+ * "committed marker file".
+ */
+bool
+UnionfsCstore::committed_marker_exists(const string& marker)
+{
+ bool ret = false;
+ ifstream fin(C_COMMITTED_MARKER_FILE.c_str());
+ while (!fin.eof() && !fin.bad() && !fin.fail()) {
+ string line;
+ getline(fin, line);
+ if (line == marker) {
+ ret = true;
+ break;
+ }
+ }
+ fin.close();
+ return ret;
+}
+
+/* recursively copy source directory to destination.
+ * will throw exception (from b_fs) if fail.
+ */
+void
+UnionfsCstore::recursive_copy_dir(const b_fs::path& src, const b_fs::path& dst)
+{
+ string src_str = src.file_string();
+ string dst_str = dst.file_string();
+ b_fs::create_directory(dst);
+
+ b_fs::recursive_directory_iterator di(src);
+ for (; di != b_fs::recursive_directory_iterator(); ++di) {
+ b_fs::path opath = di->path();
+ string nname = opath.file_string();
+ nname.replace(0, src_str.length(), dst_str);
+ b_fs::path npath = nname;
+ if (b_fs::is_directory(opath)) {
+ b_fs::create_directory(npath);
+ } else {
+ b_fs::copy_file(opath, npath);
+ }
+ }
+}
+