/* * Copyright (C) 2009-2016 Olof Hagsand and Benny Holmgren This file is part of CLICON. CLICON is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. CLICON 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 CLICON; see the file COPYING. If not, see . */ #ifdef HAVE_CONFIG_H #include "clicon_config.h" /* generated by config & autoconf */ #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* cligen */ #include /* clicon */ #include #include "clicon_backend_transaction.h" #include "backend_plugin.h" #include "backend_handle.h" #include "backend_commit.h" #include "backend_client.h" /*! Key values are checked for validity independent of user-defined callbacks * * Key values are checked as follows: * 1. If no value and default value defined, add it. * 2. If no value and mandatory flag set in spec, report error. * 3. Validate value versus spec, and report error if no match. Currently only int ranges and * string regexp checked. * See also db_lv_set() where defaults are also filled in. The case here for defaults * are if code comes via XML/NETCONF. * @param yspec Yang spec * @param td Transaction data */ static int generic_validate(yang_spec *yspec, transaction_data_t *td) { int retval = -1; cxobj *x1; cxobj *x2; int i; yang_stmt *ys; /* changed entries */ for (i=0; itd_clen; i++){ x1 = td->td_scvec[i]; /* source changed */ x2 = td->td_tcvec[i]; /* target changed */ ys = xml_spec(x1); if (xml_yang_validate(x2, ys) < 0) goto done; } /* deleted entries */ for (i=0; itd_dlen; i++){ x1 = td->td_dvec[i]; ys = xml_spec(x1); if (yang_mandatory(ys)){ clicon_err(OE_CFG, 0,"Removed mandatory variable: %s", xml_name(x1)); goto done; } } /* added entries */ for (i=0; itd_alen; i++){ x2 = td->td_avec[i]; if (xml_yang_validate(x2, xml_spec(x2)) < 0) goto done; if (xml_apply(x2, CX_ELMNT, (xml_applyfn_t*)xml_yang_validate, NULL) < 0) goto done; } retval = 0; done: return retval; } /*! Do a diff between candidate and running, then start a commit transaction * * The code reverts changes if the commit fails. But if the revert * fails, we just ignore the errors and proceed. Maybe we should * do something more drastic? * @param[in] h Clicon handle * @param[in] running The current database. The original backend state * @param[in] candidate: The candidate database. The wanted backend state */ int candidate_commit(clicon_handle h, char *candidate, char *running) { int retval = -1; // int i, j; // int failed = 0; struct stat sb; void *firsterr = NULL; yang_spec *yspec; transaction_data_t *td = NULL; if ((yspec = clicon_dbspec_yang(h)) == NULL){ clicon_err(OE_FATAL, 0, "No DB_SPEC"); goto done; } /* Sanity checks that databases exists. */ if (stat(running, &sb) < 0){ clicon_err(OE_DB, errno, "%s", running); goto done; } if (stat(candidate, &sb) < 0){ clicon_err(OE_DB, errno, "%s", candidate); goto done; } /* 1. Start transaction */ if ((td = transaction_new()) == NULL) goto done; /* 2. Parse xml trees */ if (xmldb_get(running, "/", yspec, &td->td_src) < 0) goto done; if (xmldb_get(candidate, "/", yspec, &td->td_target) < 0) goto done; /* 3. Compute differences */ if (xml_diff(yspec, td->td_src, td->td_target, &td->td_dvec, /* removed: only in running */ &td->td_dlen, &td->td_avec, /* added: only in candidate */ &td->td_alen, &td->td_scvec, /* changed: original values */ &td->td_tcvec, /* changed: wanted values */ &td->td_clen) < 0) goto done; if (debug) transaction_print(stderr, td); /* 4. Call plugin transaction start callbacks */ if (plugin_transaction_begin(h, td) < 0) goto done; /* 5. Make generic validation on all new or changed data. */ if (generic_validate(yspec, td) < 0) goto done; /* 6. Call plugin transaction validate callbacks */ if (plugin_transaction_validate(h, td) < 0) goto done; /* 7. Call plugin transaction complete callbacks */ if (plugin_transaction_complete(h, td) < 0) goto done; /* 7. Call plugin transaction commit callbacks */ if (plugin_transaction_commit(h, td) < 0) goto done; /* 8. Copy running back to candidate in case end functions triggered updates in running */ if (file_cp(running, candidate) < 0){ /* ignore errors or signal major setback ? */ clicon_err(OE_UNIX, errno, "file_cp(running, candidate)"); clicon_log(LOG_NOTICE, "Error in rollback, trying to continue"); goto done; } /* 9. Call plugin transaction end callbacks */ plugin_transaction_end(h, td); #ifdef OBSOLETE /* Find the differences between the two databases and store it in df vector. */ memset(&df, 0, sizeof(df)); if (db_diff(running, candidate, __FUNCTION__, clicon_dbspec_key(h), &df ) < 0) goto done; if (debug){ struct dbdiff_ent *dfe; for (i=0; idfe_op, cvec_name_get(dfe->dfe_vec1?dfe->dfe_vec1:dfe->dfe_vec2)); } } /* 1. Get commit processing to dbdiff vector: one entry per key that changed. changes are registered as if they exist in the 1st(candidate) or 2nd(running) dbs. */ if (dbdep_commitvec(h, &df, &nvec, &ddvec) < 0) goto done; /* 2. Call transaction_begin hooks */ if (plugin_transaction_begin(h) < 0) goto done; /* call generic cv_validate() on all new or changed keys. */ if (generic_validate(yspec, NULL) < 0) goto done; /* user-defined validate callbacks registered in dbdep_validate */ // if (validate_db(h, nvec, ddvec, running, candidate) < 0) // goto done; /* Call plugin post-commit hooks */ if (plugin_transaction_complete(h) < 0) goto done; if (clicon_commit_order(h) == 0){ for (i=0; i < nvec; i++){ /* revert in opposite order */ dd = &ddvec[i]; dp = dd->dd_dep; /* op, callback, arg */ if ((dp->dp_type & TRANS_CB_COMMIT) == 0) continue; dfe = dd->dd_dbdiff; /* key1/key2/op */ op = dbdiff2commit_op(dfe->dfe_op); if (plugin_commit_callback(h, op, /* oper */ running, /* db1 */ candidate, /* db2 */ dd->dd_mkey1, /* key1 */ dd->dd_mkey2, /* key2 */ dd->dd_dbdiff->dfe_vec1, /* vec1 */ dd->dd_dbdiff->dfe_vec2, /* vec2 */ dp /* callback */ ) < 0){ firsterr = clicon_err_save(); /* save this error */ failed++; break; } } if (!failed) if (file_cp(candidate, running) < 0){ /* Commit here in case cp fails */ clicon_err(OE_UNIX, errno, "file_cp"); failed++; } /* Failed operation, start error handling: rollback in opposite order */ if (failed){ for (j=i-1; j>=0; j--){ /* revert in opposite order */ dd = &ddvec[j]; dp = dd->dd_dep; /* op, callback, arg */ if ((dp->dp_type & TRANS_CB_COMMIT) == 0) continue; dfe = dd->dd_dbdiff; /* key1/key2/op */ op = dbdiff2commit_op(dfe->dfe_op); switch (op){ /* reverse operation */ case CO_ADD: op = CO_DELETE; break; case CO_DELETE: op = CO_ADD; break; default: break; } if (plugin_commit_callback(h, op, /* oper */ candidate, /* db1 */ running, /* db2 */ dd->dd_mkey2, /* key1 */ dd->dd_mkey1, /* key2 */ dd->dd_dbdiff->dfe_vec2, /* vec1 */ dd->dd_dbdiff->dfe_vec1, /* vec2 */ dp /* callback */ ) < 0){ /* ignore errors or signal major setback ? */ clicon_log(LOG_NOTICE, "Error in rollback, trying to continue"); continue; } } goto done; } /* error handling */ } else { /* commit_order == 1 or 2 */ /* Now follows commit rules in order. * 4. For all keys that are not in candidate but in running, delete key * in reverse prio order */ for (i = nvec-1; i >= 0; i--){ dd = &ddvec[i]; dp = dd->dd_dep; /* op, callback, arg */ if ((dp->dp_type & TRANS_CB_COMMIT) == 0) continue; dfe = dd->dd_dbdiff; /* key1/key2/op */ op = dbdiff2commit_op(dfe->dfe_op); /* original mode 2 where CHANGE=DEL/ADD */ if (clicon_commit_order(h) == 2 && op == CO_CHANGE) op = CO_DELETE; if (op != CO_DELETE) continue; if (plugin_commit_callback(h, op, /* oper */ running, /* db1 */ candidate, /* db2 */ dd->dd_mkey1, /* key1 */ dd->dd_mkey2, /* key2 */ dd->dd_dbdiff->dfe_vec1, /* vec1 */ dd->dd_dbdiff->dfe_vec2, /* vec2 */ dp /* callback */ ) < 0){ firsterr = clicon_err_save(); /* save this error */ break; } } /* 5. Failed deletion, add the key value back to running */ if (i >= 0){ /* failed */ for (j=i+1; jdd_dep; /* op, callback, arg */ if ((dp->dp_type & TRANS_CB_COMMIT) == 0) continue; dfe = dd->dd_dbdiff; /* key1/key2/op */ op = dbdiff2commit_op(dfe->dfe_op); /* original mode 2 where CHANGE=DEL/ADD */ if (clicon_commit_order(h) == 2 && op == CO_CHANGE) op = CO_DELETE; if (op != CO_DELETE) continue; if (plugin_commit_callback(h, op, /* oper */ candidate, /* db1 */ running, /* db2 */ dd->dd_mkey2, /* key1 */ dd->dd_mkey1, /* key2 */ dd->dd_dbdiff->dfe_vec2, /* vec1 */ dd->dd_dbdiff->dfe_vec1, /* vec2 */ dp /* callback */ ) < 0){ /* ignore errors or signal major setback ? */ clicon_log(LOG_NOTICE, "Error in rollback, trying to continue"); continue; } } goto done; } /* * 6. For all added or changed keys */ for (i=0; i < nvec; i++){ dd = &ddvec[i]; dp = dd->dd_dep; /* op, callback, arg */ if ((dp->dp_type & TRANS_CB_COMMIT) == 0) continue; dfe = dd->dd_dbdiff; /* key1/key2/op */ op = dbdiff2commit_op(dfe->dfe_op); if (op != CO_CHANGE && op != CO_ADD) continue; /* original mode 2 where CHANGE=DEL/ADD */ if (clicon_commit_order(h) == 2 && op == CO_CHANGE) op = CO_ADD; if (plugin_commit_callback(h, op, /* oper */ running, /* db1 */ candidate, /* db2 */ dd->dd_mkey1, /* key1 */ dd->dd_mkey2, /* key2 */ dd->dd_dbdiff->dfe_vec1, /* vec1 */ dd->dd_dbdiff->dfe_vec2, /* vec2 */ dp /* callback */ ) < 0){ firsterr = clicon_err_save(); /* save this error */ failed++; break; } } if (!failed) /* Commit here in case cp fails */ if (file_cp(candidate, running) < 0){ clicon_err(OE_UNIX, errno, "file_cp(candidate; running)"); failed++; } /* 10. Failed setting keys in running, first remove the keys set */ if (failed){ /* failed */ for (j=i-1; j>=0; j--){ /* revert in opposite order */ dd = &ddvec[j]; dp = dd->dd_dep; /* op, callback, arg */ if ((dp->dp_type & TRANS_CB_COMMIT) == 0) continue; dfe = dd->dd_dbdiff; /* key1/key2/op */ op = dbdiff2commit_op(dfe->dfe_op); if (op != CO_CHANGE && op != CO_ADD) continue; /* original mode 2 where CHANGE=DEL/ADD */ if (clicon_commit_order(h) == 2 && op == CO_CHANGE) op = CO_ADD; if (op == CO_ADD) /* reverse op */ op = CO_DELETE; if (plugin_commit_callback(h, op, /* oper */ candidate, /* db1 */ running, /* db2 */ dd->dd_mkey2, /* key1 */ dd->dd_mkey1, /* key2 */ dd->dd_dbdiff->dfe_vec2, /* vec1 */ dd->dd_dbdiff->dfe_vec1, /* vec2 */ dp /* callback */ ) < 0){ /* ignore errors or signal major setback ? */ clicon_log(LOG_NOTICE, "Error in rollback, trying to continue"); continue; } } for (j=0; j < nvec; j++){ /* revert in opposite order */ dd = &ddvec[j]; dp = dd->dd_dep; /* op, callback, arg */ if ((dp->dp_type & TRANS_CB_COMMIT) == 0) continue; dfe = dd->dd_dbdiff; /* key1/key2/op */ op = dbdiff2commit_op(dfe->dfe_op); /* original mode 2 where CHANGE=DEL/ADD */ if (clicon_commit_order(h) == 2 && op == CO_CHANGE) op = CO_DELETE; if (op != CO_DELETE) continue; op = CO_ADD; if (plugin_commit_callback(h, op, /* oper */ candidate, /* db1 */ running, /* db2 */ dd->dd_mkey2, /* key1 */ dd->dd_mkey1, /* key2 */ dd->dd_dbdiff->dfe_vec2, /* vec1 */ dd->dd_dbdiff->dfe_vec1, /* vec2 */ dp /* callback */ ) < 0){ /* ignore errors or signal major setback ? */ clicon_log(LOG_NOTICE, "Error in rollback, trying to continue"); continue; } } goto done; } } /* commit_order */ #endif /* OBSOLETE */ retval = 0; done: /* In case of failure, call plugin transaction termination callbacks */ if (retval < 0 && td) plugin_transaction_abort(h, td); if (td) transaction_free(td); if (firsterr) clicon_err_restore(firsterr); return retval; } /*! Do a diff between candidate and running, then start a validate transaction * * @param[in] h Clicon handle * @param[in] running The current database. The original backend state * @param[in] candidate: The candidate database. The wanted backend state */ int candidate_validate(clicon_handle h, char *candidate, char *running) { int retval = -1; struct stat sb; yang_spec *yspec; transaction_data_t *td = NULL; if ((yspec = clicon_dbspec_yang(h)) == NULL){ clicon_err(OE_FATAL, 0, "No DB_SPEC"); goto done; } /* Sanity checks that databases exists. */ if (stat(running, &sb) < 0){ clicon_err(OE_DB, errno, "%s", running); goto done; } if (stat(candidate, &sb) < 0){ clicon_err(OE_DB, errno, "%s", candidate); goto done; } /* 1. Start transaction */ if ((td = transaction_new()) == NULL) goto done; /* 2. Parse xml trees */ if (xmldb_get(running, "/", yspec, &td->td_src) < 0) goto done; if (xmldb_get(candidate, "/", yspec, &td->td_target) < 0) goto done; /* 3. Compute differences */ if (xml_diff(yspec, td->td_src, td->td_target, &td->td_dvec, /* removed: only in running */ &td->td_dlen, &td->td_avec, /* added: only in candidate */ &td->td_alen, &td->td_scvec, /* changed: original values */ &td->td_tcvec, /* changed: wanted values */ &td->td_clen) < 0) goto done; if (debug) transaction_print(stderr, td); /* 4. Call plugin start transaction callbacks */ if (plugin_transaction_begin(h, td) < 0) goto done; /* 5. Make generic validation on all new or changed data. */ if (generic_validate(yspec, td) < 0) goto done; /* 6. Call plugin validate transaction callbacks */ if (plugin_transaction_validate(h, td) < 0) goto done; /* 7. Call plugin complete transaction callbacks */ if (plugin_transaction_complete(h, td) < 0) goto done; retval = 0; done: /* In case of failure, call plugin transaction termination callbacks */ if (retval < 0 && td) plugin_transaction_abort(h, td); if (td) transaction_free(td); return retval; } /*! Handle an incoming commit message from a client. * XXX: If commit succeeds and snapshot/startup fails, we have strange state: * the commit has succeeded but an error message is returned. */ int from_client_commit(clicon_handle h, int s, struct clicon_msg *msg, const char *label) { int retval = -1; char *candidate; char *running; uint32_t snapshot; uint32_t startup; char *snapshot_0; char *archive_dir; char *startup_config; if (clicon_msg_commit_decode(msg, &candidate, &running, &snapshot, &startup, label) < 0) goto err; if (candidate_commit(h, candidate, running) < 0){ clicon_debug(1, "Commit %s failed", candidate); retval = 0; /* We ignore errors from commit, but maybe we should fail on fatal errors? */ goto err; } clicon_debug(1, "Commit %s", candidate); if (snapshot){ if ((archive_dir = clicon_archive_dir(h)) == NULL){ clicon_err(OE_PLUGIN, 0, "snapshot set and clicon_archive_dir not defined"); goto err; } if (config_snapshot(h, running, archive_dir) < 0) goto err; } if (startup){ if ((archive_dir = clicon_archive_dir(h)) == NULL){ clicon_err(OE_PLUGIN, 0, "startup set but clicon_archive_dir not defined"); goto err; } if ((startup_config = clicon_startup_config(h)) == NULL){ clicon_err(OE_PLUGIN, 0, "startup set but startup_config not defined"); goto err; } snapshot_0 = chunk_sprintf(__FUNCTION__, "%s/0", archive_dir); if (file_cp(snapshot_0, startup_config) < 0){ clicon_err(OE_PROTO, errno, "%s: Error when creating startup", __FUNCTION__); goto err; } } retval = 0; if (send_msg_ok(s) < 0) goto done; goto done; err: /* XXX: more elaborate errstring? */ if (send_msg_err(s, clicon_errno, clicon_suberrno, "%s", clicon_err_reason) < 0) retval = -1; done: unchunk_group(__FUNCTION__); return retval; /* may be zero if we ignoring errors from commit */ } /* from_client_commit */ /* * Call backend plugin */ int from_client_validate(clicon_handle h, int s, struct clicon_msg *msg, const char *label) { char *dbname; char *running_db; int retval = -1; if (clicon_msg_validate_decode(msg, &dbname, label) < 0){ send_msg_err(s, clicon_errno, clicon_suberrno, clicon_err_reason); goto err; } clicon_debug(1, "Validate %s", dbname); if ((running_db = clicon_running_db(h)) == NULL){ clicon_err(OE_FATAL, 0, "running db not set"); goto err; } if (candidate_validate(h, dbname, running_db) < 0){ clicon_debug(1, "Validate %s failed", dbname); retval = 0; /* We ignore errors from commit, but maybe we should fail on fatal errors? */ goto err; } retval = 0; if (send_msg_ok(s) < 0) goto done; goto done; err: /* XXX: more elaborate errstring? */ if (send_msg_err(s, clicon_errno, clicon_suberrno, "%s", clicon_err_reason) < 0) retval = -1; done: unchunk_group(__FUNCTION__); return retval; } /* from_client_validate */