diff --git a/lib/common/acl.c b/lib/common/acl.c index 9e2205980c..4d430127e3 100644 --- a/lib/common/acl.c +++ b/lib/common/acl.c @@ -1,930 +1,936 @@ /* * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include // xmlNode, etc. #include // xmlChar #include // xmlXPathObject, etc. #include #include #include #include "crmcommon_private.h" typedef struct xml_acl_s { enum pcmk__xml_flags mode; gchar *xpath; } xml_acl_t; static void free_acl(void *data) { if (data) { xml_acl_t *acl = data; g_free(acl->xpath); free(acl); } } void pcmk__free_acls(GList *acls) { g_list_free_full(acls, free_acl); } static GList * create_acl(const xmlNode *xml, GList *acls, enum pcmk__xml_flags mode) { xml_acl_t *acl = NULL; const char *tag = pcmk__xe_get(xml, PCMK_XA_OBJECT_TYPE); const char *ref = pcmk__xe_get(xml, PCMK_XA_REFERENCE); const char *xpath = pcmk__xe_get(xml, PCMK_XA_XPATH); const char *attr = pcmk__xe_get(xml, PCMK_XA_ATTRIBUTE); if ((tag == NULL) && (ref == NULL) && (xpath == NULL)) { // Schema should prevent this, but to be safe ... crm_trace("Ignoring ACL <%s> element without selection criteria", xml->name); return NULL; } acl = pcmk__assert_alloc(1, sizeof (xml_acl_t)); acl->mode = mode; if (xpath) { acl->xpath = g_strdup(xpath); crm_trace("Unpacked ACL <%s> element using xpath: %s", xml->name, acl->xpath); } else { GString *buf = g_string_sized_new(128); if ((ref != NULL) && (attr != NULL)) { // NOTE: schema currently does not allow this pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" PCMK_XA_ID "='", ref, "' and @", attr, "]", NULL); } else if (ref != NULL) { pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" PCMK_XA_ID "='", ref, "']", NULL); } else if (attr != NULL) { pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@", attr, "]", NULL); } else { pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), NULL); } acl->xpath = buf->str; g_string_free(buf, FALSE); crm_trace("Unpacked ACL <%s> element as xpath: %s", xml->name, acl->xpath); } return g_list_append(acls, acl); } /*! * \internal * \brief Unpack a user, group, or role subtree of the ACLs section * * \param[in] acl_top XML of entire ACLs section * \param[in] acl_entry XML of ACL element being unpacked * \param[in,out] acls List of ACLs unpacked so far * * \return New head of (possibly modified) acls * * \note This function is recursive */ static GList * parse_acl_entry(const xmlNode *acl_top, const xmlNode *acl_entry, GList *acls) { for (const xmlNode *child = pcmk__xe_first_child(acl_entry, NULL, NULL, NULL); child != NULL; child = pcmk__xe_next(child, NULL)) { if (pcmk__xe_is(child, PCMK_XE_ACL_PERMISSION)) { const char *kind = pcmk__xe_get(child, PCMK_XA_KIND); pcmk__assert(kind != NULL); crm_trace("Unpacking <" PCMK_XE_ACL_PERMISSION "> element of " "kind '%s'", kind); if (pcmk__str_eq(kind, PCMK_VALUE_READ, pcmk__str_none)) { acls = create_acl(child, acls, pcmk__xf_acl_read); } else if (pcmk__str_eq(kind, PCMK_VALUE_WRITE, pcmk__str_none)) { acls = create_acl(child, acls, pcmk__xf_acl_write); } else if (pcmk__str_eq(kind, PCMK_VALUE_DENY, pcmk__str_none)) { acls = create_acl(child, acls, pcmk__xf_acl_deny); } else { crm_warn("Ignoring unknown ACL kind '%s'", kind); } } else if (pcmk__xe_is(child, PCMK_XE_ROLE)) { const char *ref_role = pcmk__xe_get(child, PCMK_XA_ID); crm_trace("Unpacking <" PCMK_XE_ROLE "> element"); if (ref_role == NULL) { continue; } for (xmlNode *role = pcmk__xe_first_child(acl_top, NULL, NULL, NULL); role != NULL; role = pcmk__xe_next(role, NULL)) { const char *role_id = NULL; if (!pcmk__xe_is(role, PCMK_XE_ACL_ROLE)) { continue; } role_id = pcmk__xe_get(role, PCMK_XA_ID); if (pcmk__str_eq(ref_role, role_id, pcmk__str_none)) { crm_trace("Unpacking referenced role '%s' in <%s> element", role_id, acl_entry->name); acls = parse_acl_entry(acl_top, role, acls); break; } } } } return acls; } /* */ static const char * acl_to_text(enum pcmk__xml_flags flags) { if (pcmk_is_set(flags, pcmk__xf_acl_deny)) { return "deny"; } else if (pcmk_any_flags_set(flags, pcmk__xf_acl_write|pcmk__xf_acl_create)) { return "read/write"; } else if (pcmk_is_set(flags, pcmk__xf_acl_read)) { return "read"; } return "none"; } void pcmk__apply_acl(xmlNode *xml) { GList *aIter = NULL; xml_doc_private_t *docpriv = NULL; xml_node_private_t *nodepriv = NULL; xmlXPathObject *xpathObj = NULL; pcmk__assert(xml != NULL); docpriv = xml->doc->_private; if (!pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_acl_enabled)) { crm_trace("Skipping ACLs for user '%s' because not enabled for this XML", pcmk__s(docpriv->acl_user, "(unknown)")); return; } for (aIter = docpriv->acls; aIter != NULL; aIter = aIter->next) { int max = 0, lpc = 0; xml_acl_t *acl = aIter->data; xpathObj = pcmk__xpath_search(xml->doc, acl->xpath); max = pcmk__xpath_num_results(xpathObj); for (lpc = 0; lpc < max; lpc++) { xmlNode *match = pcmk__xpath_result(xpathObj, lpc); if (match == NULL) { continue; } /* @COMPAT If the ACL's XPath matches a node that is neither an * element nor a document, we apply the ACL to the parent element * rather than to the matched node. For example, if the XPath * matches a "score" attribute, then it applies to every element * that contains a "score" attribute. That is, the XPath expression * "//@score" matches all attributes named "score", but we apply the * ACL to all elements containing such an attribute. * * This behavior is incorrect from an XPath standpoint and is thus * confusing and counterintuitive. The correct way to match all * elements containing a "score" attribute is to use an XPath * predicate: "// *[@score]". (Space inserted after slashes so that * GCC doesn't throw an error about nested comments.) * * Additionally, if an XPath expression matches the entire document * (for example, "/"), then the ACL applies to the document's root * element if it exists. * * These behaviors should be changed so that the ACL applies to the * nodes matched by the XPath expression, or so that it doesn't * apply at all if applying an ACL to an attribute doesn't make * sense. * * Unfortunately, we document in Pacemaker Explained that matching * attributes is a valid way to match elements: "Attributes may be * specified in the XPath to select particular elements, but the * permissions apply to the entire element." * * So we have to keep this behavior at least until a compatibility * break. Even then, it's not feasible in the general case to * transform such XPath expressions using XSLT. */ match = pcmk__xpath_match_element(match); if (match == NULL) { continue; } nodepriv = match->_private; pcmk__set_xml_flags(nodepriv, acl->mode); // Build a GString only if tracing is enabled pcmk__if_tracing( { GString *path = pcmk__element_xpath(match); crm_trace("Applying %s ACL to %s matched by %s", acl_to_text(acl->mode), path->str, acl->xpath); g_string_free(path, TRUE); }, {} ); } crm_trace("Applied %s ACL %s (%d match%s)", acl_to_text(acl->mode), acl->xpath, max, ((max == 1)? "" : "es")); xmlXPathFreeObject(xpathObj); } } /*! * \internal * \brief Unpack ACLs for a given user into the * metadata of the target XML tree * * Taking the description of ACLs from the source XML tree and * marking up the target XML tree with access information for the * given user by tacking it onto the relevant nodes * * \param[in] source XML with ACL definitions * \param[in,out] target XML that ACLs will be applied to * \param[in] user Username whose ACLs need to be unpacked */ void pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user) { xml_doc_private_t *docpriv = NULL; if ((target == NULL) || (target->doc == NULL) || (target->doc->_private == NULL)) { return; } docpriv = target->doc->_private; if (!pcmk_acl_required(user)) { crm_trace("Not unpacking ACLs because not required for user '%s'", user); } else if (docpriv->acls == NULL) { xmlNode *acls = pcmk__xpath_find_one(source->doc, "//" PCMK_XE_ACLS, LOG_NEVER); pcmk__str_update(&(docpriv->acl_user), user); if (acls) { xmlNode *child = NULL; for (child = pcmk__xe_first_child(acls, NULL, NULL, NULL); child != NULL; child = pcmk__xe_next(child, NULL)) { if (pcmk__xe_is(child, PCMK_XE_ACL_TARGET)) { const char *id = pcmk__xe_get(child, PCMK_XA_NAME); if (id == NULL) { id = pcmk__xe_get(child, PCMK_XA_ID); } if (id && strcmp(id, user) == 0) { crm_debug("Unpacking ACLs for user '%s'", id); docpriv->acls = parse_acl_entry(acls, child, docpriv->acls); } } else if (pcmk__xe_is(child, PCMK_XE_ACL_GROUP)) { const char *id = pcmk__xe_get(child, PCMK_XA_NAME); if (id == NULL) { id = pcmk__xe_get(child, PCMK_XA_ID); } if (id && pcmk__is_user_in_group(user,id)) { crm_debug("Unpacking ACLs for group '%s'", id); docpriv->acls = parse_acl_entry(acls, child, docpriv->acls); } } } } } } /*! * \internal * \brief Copy source to target and set xf_acl_enabled flag in target * * \param[in] acl_source XML with ACL definitions * \param[in,out] target XML that ACLs will be applied to * \param[in] user Username whose ACLs need to be set */ void pcmk__enable_acl(xmlNode *acl_source, xmlNode *target, const char *user) { if (target == NULL) { return; } pcmk__unpack_acl(acl_source, target, user); pcmk__xml_doc_set_flags(target->doc, pcmk__xf_acl_enabled); pcmk__apply_acl(target); } static inline bool test_acl_mode(enum pcmk__xml_flags allowed, enum pcmk__xml_flags requested) { if (pcmk_is_set(allowed, pcmk__xf_acl_deny)) { return false; } else if (pcmk_all_flags_set(allowed, requested)) { return true; } else if (pcmk_is_set(requested, pcmk__xf_acl_read) && pcmk_is_set(allowed, pcmk__xf_acl_write)) { return true; } else if (pcmk_is_set(requested, pcmk__xf_acl_create) && pcmk_any_flags_set(allowed, pcmk__xf_acl_write|pcmk__xf_created)) { return true; } return false; } /*! * \internal * \brief Rid XML tree of all unreadable nodes and node properties * * \param[in,out] xml Root XML node to be purged of attributes * * \return true if this node or any of its children are readable * if false is returned, xml will be freed * * \note This function is recursive */ static bool purge_xml_attributes(xmlNode *xml) { xmlNode *child = NULL; xmlAttr *xIter = NULL; bool readable_children = false; xml_node_private_t *nodepriv = xml->_private; if (test_acl_mode(nodepriv->flags, pcmk__xf_acl_read)) { crm_trace("%s[@" PCMK_XA_ID "=%s] is readable", xml->name, pcmk__xe_id(xml)); return true; } xIter = xml->properties; while (xIter != NULL) { xmlAttr *tmp = xIter; const char *prop_name = (const char *)xIter->name; xIter = xIter->next; if (strcmp(prop_name, PCMK_XA_ID) == 0) { continue; } pcmk__xa_remove(tmp, true); } child = pcmk__xml_first_child(xml); while ( child != NULL ) { xmlNode *tmp = child; child = pcmk__xml_next(child); readable_children |= purge_xml_attributes(tmp); } if (!readable_children) { // Nothing readable under here, so purge completely pcmk__xml_free(xml); } return readable_children; } /*! * \brief Copy ACL-allowed portions of specified XML * * \param[in] user Username whose ACLs should be used * \param[in] acl_source XML containing ACLs * \param[in] xml XML to be copied * \param[out] result Copy of XML portions readable via ACLs * * \return true if xml exists and ACLs are required for user, false otherwise * \note If this returns true, caller should use \p result rather than \p xml */ bool xml_acl_filtered_copy(const char *user, xmlNode *acl_source, xmlNode *xml, xmlNode **result) { GList *aIter = NULL; xmlNode *target = NULL; xml_doc_private_t *docpriv = NULL; *result = NULL; if ((xml == NULL) || !pcmk_acl_required(user)) { crm_trace("Not filtering XML because ACLs not required for user '%s'", user); return false; } crm_trace("Filtering XML copy using user '%s' ACLs", user); target = pcmk__xml_copy(NULL, xml); if (target == NULL) { return true; } pcmk__enable_acl(acl_source, target, user); docpriv = target->doc->_private; for(aIter = docpriv->acls; aIter != NULL && target; aIter = aIter->next) { int max = 0; xml_acl_t *acl = aIter->data; if (acl->mode != pcmk__xf_acl_deny) { /* Nothing to do */ } else if (acl->xpath) { int lpc = 0; xmlXPathObject *xpathObj = pcmk__xpath_search(target->doc, acl->xpath); max = pcmk__xpath_num_results(xpathObj); for(lpc = 0; lpc < max; lpc++) { xmlNode *match = pcmk__xpath_result(xpathObj, lpc); if (match == NULL) { continue; } // @COMPAT See COMPAT comment in pcmk__apply_acl() match = pcmk__xpath_match_element(match); if (match == NULL) { continue; } if (!purge_xml_attributes(match) && (match == target)) { crm_trace("ACLs deny user '%s' access to entire XML document", user); xmlXPathFreeObject(xpathObj); return true; } } crm_trace("ACLs deny user '%s' access to %s (%d %s)", user, acl->xpath, max, pcmk__plural_alt(max, "match", "matches")); xmlXPathFreeObject(xpathObj); } } if (!purge_xml_attributes(target)) { crm_trace("ACLs deny user '%s' access to entire XML document", user); return true; } if (docpriv->acls) { g_list_free_full(docpriv->acls, free_acl); docpriv->acls = NULL; } else { crm_trace("User '%s' without ACLs denied access to entire XML document", user); pcmk__xml_free(target); target = NULL; } if (target) { *result = target; } return true; } /*! * \internal * \brief Check whether creation of an XML element is implicitly allowed * * Check whether XML is a "scaffolding" element whose creation is implicitly * allowed regardless of ACLs (that is, it is not in the ACL section and has * no attributes other than \c PCMK_XA_ID). * * \param[in] xml XML element to check * * \return true if XML element is implicitly allowed, false otherwise */ static bool implicitly_allowed(const xmlNode *xml) { GString *path = NULL; for (xmlAttr *prop = xml->properties; prop != NULL; prop = prop->next) { if (strcmp((const char *) prop->name, PCMK_XA_ID) != 0) { return false; } } path = pcmk__element_xpath(xml); pcmk__assert(path != NULL); if (strstr((const char *) path->str, "/" PCMK_XE_ACLS "/") != NULL) { g_string_free(path, TRUE); return false; } g_string_free(path, TRUE); return true; } #define display_id(xml) pcmk__s(pcmk__xe_id(xml), "") /*! * \internal * \brief Drop XML nodes created in violation of ACLs * * Given an XML element, free all of its descendant nodes created in violation * of ACLs, with the exception of allowing "scaffolding" elements (i.e. those * that aren't in the ACL section and don't have any attributes other than * \c PCMK_XA_ID). * * \param[in,out] xml XML to check * \param[in] check_top Whether to apply checks to argument itself * (if true, xml might get freed) * * \note This function is recursive */ void pcmk__apply_creation_acl(xmlNode *xml, bool check_top) { xml_node_private_t *nodepriv = xml->_private; if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { if (implicitly_allowed(xml)) { crm_trace("Creation of <%s> scaffolding with " PCMK_XA_ID "=\"%s\"" " is implicitly allowed", xml->name, display_id(xml)); } else if (pcmk__check_acl(xml, NULL, pcmk__xf_acl_write)) { crm_trace("ACLs allow creation of <%s> with " PCMK_XA_ID "=\"%s\"", xml->name, display_id(xml)); } else if (check_top) { /* is_root=true should be impossible with check_top=true, but check * for sanity */ bool is_root = (xmlDocGetRootElement(xml->doc) == xml); xml_doc_private_t *docpriv = xml->doc->_private; crm_trace("ACLs disallow creation of %s<%s> with " PCMK_XA_ID "=\"%s\"", (is_root? "root element " : ""), xml->name, display_id(xml)); // pcmk__xml_free() checks ACLs if enabled, which would fail pcmk__clear_xml_flags(docpriv, pcmk__xf_acl_enabled); pcmk__xml_free(xml); if (!is_root) { // If root, the document was freed. Otherwise re-enable ACLs. pcmk__set_xml_flags(docpriv, pcmk__xf_acl_enabled); } return; } else { crm_notice("ACLs would disallow creation of %s<%s> with " PCMK_XA_ID "=\"%s\"", ((xml == xmlDocGetRootElement(xml->doc))? "root element " : ""), xml->name, display_id(xml)); } } for (xmlNode *cIter = pcmk__xml_first_child(xml); cIter != NULL; ) { xmlNode *child = cIter; cIter = pcmk__xml_next(cIter); /* In case it is free'd */ pcmk__apply_creation_acl(child, true); } } /*! * \brief Check whether or not an XML node is ACL-denied * * \param[in] xml node to check * * \return true if XML node exists and is ACL-denied, false otherwise */ bool xml_acl_denied(const xmlNode *xml) { if (xml && xml->doc && xml->doc->_private){ xml_doc_private_t *docpriv = xml->doc->_private; return pcmk_is_set(docpriv->flags, pcmk__xf_acl_denied); } return false; } void xml_acl_disable(xmlNode *xml) { if ((xml != NULL) && pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_acl_enabled)) { xml_doc_private_t *docpriv = xml->doc->_private; /* Catch anything that was created but shouldn't have been */ pcmk__apply_acl(xml); pcmk__apply_creation_acl(xml, false); pcmk__clear_xml_flags(docpriv, pcmk__xf_acl_enabled); } } /*! * \internal * \brief Deny access to an XML tree's document based on ACLs * * \param[in,out] xml XML tree * \param[in] attr_name Name of attribute being accessed in \p xml (for * logging only) * \param[in] prefix Prefix describing ACL that denied access (for * logging only) * \param[in] user User accessing \p xml (for logging only) * \param[in] mode Access mode (for logging only) */ #define check_acl_deny(xml, attr_name, prefix, user, mode) do { \ xmlNode *tree = xml; \ \ pcmk__xml_doc_set_flags(tree->doc, pcmk__xf_acl_denied); \ pcmk__if_tracing( \ { \ GString *xpath = pcmk__element_xpath(tree); \ \ if ((attr_name) != NULL) { \ pcmk__g_strcat(xpath, "[@", attr_name, "]", NULL); \ } \ qb_log_from_external_source(__func__, __FILE__, \ "%sACL denies user '%s' %s " \ "access to %s", \ LOG_TRACE, __LINE__, 0 , \ prefix, user, \ acl_to_text(mode), xpath->str); \ g_string_free(xpath, TRUE); \ }, \ {} \ ); \ } while (false); bool pcmk__check_acl(xmlNode *xml, const char *attr_name, enum pcmk__xml_flags mode) { xml_doc_private_t *docpriv = NULL; pcmk__assert((xml != NULL) && (xml->doc->_private != NULL)); if (!pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_tracking|pcmk__xf_acl_enabled)) { return true; } docpriv = xml->doc->_private; if (docpriv->acls == NULL) { check_acl_deny(xml, attr_name, "Lack of ", docpriv->acl_user, mode); return false; } /* Walk the tree upwards looking for xml_acl_* flags * - Creating an attribute requires write permissions for the node * - Creating a child requires write permissions for the parent */ if (attr_name != NULL) { xmlAttr *attr = xmlHasProp(xml, (const xmlChar *) attr_name); if ((attr != NULL) && (mode == pcmk__xf_acl_create)) { mode = pcmk__xf_acl_write; } } for (const xmlNode *parent = xml; (parent != NULL) && (parent->_private != NULL); parent = parent->parent) { const xml_node_private_t *nodepriv = parent->_private; if (test_acl_mode(nodepriv->flags, mode)) { return true; } if (pcmk_is_set(nodepriv->flags, pcmk__xf_acl_deny)) { const char *pfx = (parent != xml)? "Parent " : ""; check_acl_deny(xml, attr_name, pfx, docpriv->acl_user, mode); return false; } } check_acl_deny(xml, attr_name, "Default ", docpriv->acl_user, mode); return false; } /*! * \brief Check whether ACLs are required for a given user * * \param[in] User name to check * * \return true if the user requires ACLs, false otherwise */ bool pcmk_acl_required(const char *user) { if (pcmk__str_empty(user)) { crm_trace("ACLs not required because no user set"); return false; } else if (!strcmp(user, CRM_DAEMON_USER) || !strcmp(user, "root")) { crm_trace("ACLs not required for privileged user %s", user); return false; } crm_trace("ACLs required for %s", user); return true; } char * pcmk__uid2username(uid_t uid) { - struct passwd *pwent = getpwuid(uid); + struct passwd *pwent = NULL; + + errno = 0; + pwent = getpwuid(uid); if (pwent == NULL) { - crm_perror(LOG_INFO, "Cannot get user details for user ID %d", uid); + crm_err("Cannot get name from password database for user ID %lld: %s", + (long long) uid, + ((errno != 0)? strerror(errno) : "No matching entry found")); return NULL; } + return pcmk__str_copy(pwent->pw_name); } /*! * \internal * \brief Set the ACL user field properly on an XML request * * Multiple user names are potentially involved in an XML request: the effective * user of the current process; the user name known from an IPC client * connection; and the user name obtained from the request itself, whether by * the current standard XML attribute name or an older legacy attribute name. * This function chooses the appropriate one that should be used for ACLs, sets * it in the request (using the standard attribute name, and the legacy name if * given), and returns it. * * \param[in,out] request XML request to update * \param[in] field Alternate name for ACL user name XML attribute * \param[in] peer_user User name as known from IPC connection * * \return ACL user name actually used */ const char * pcmk__update_acl_user(xmlNode *request, const char *field, const char *peer_user) { static const char *effective_user = NULL; const char *requested_user = NULL; const char *user = NULL; if (effective_user == NULL) { effective_user = pcmk__uid2username(geteuid()); if (effective_user == NULL) { effective_user = pcmk__str_copy("#unprivileged"); crm_err("Unable to determine effective user, assuming unprivileged for ACLs"); } } requested_user = pcmk__xe_get(request, PCMK__XA_ACL_TARGET); if (requested_user == NULL) { /* Currently, different XML attribute names are used for the ACL user in * different contexts (PCMK__XA_ATTR_USER, PCMK__XA_CIB_USER, etc.). * The caller may specify that name as the field argument. * * @TODO Standardize on PCMK__XA_ACL_TARGET and eventually drop the * others once rolling upgrades from versions older than that are no * longer supported. */ requested_user = pcmk__xe_get(request, field); } if (!pcmk__is_privileged(effective_user)) { /* We're not running as a privileged user, set or overwrite any existing * value for PCMK__XA_ACL_TARGET */ user = effective_user; } else if (peer_user == NULL && requested_user == NULL) { /* No user known or requested, use 'effective_user' and make sure one is * set for the request */ user = effective_user; } else if (peer_user == NULL) { /* No user known, trusting 'requested_user' */ user = requested_user; } else if (!pcmk__is_privileged(peer_user)) { /* The peer is not a privileged user, set or overwrite any existing * value for PCMK__XA_ACL_TARGET */ user = peer_user; } else if (requested_user == NULL) { /* Even if we're privileged, make sure there is always a value set */ user = peer_user; } else { /* Legal delegation to 'requested_user' */ user = requested_user; } // This requires pointer comparison, not string comparison if (user != pcmk__xe_get(request, PCMK__XA_ACL_TARGET)) { crm_xml_add(request, PCMK__XA_ACL_TARGET, user); } if ((field != NULL) && (user != pcmk__xe_get(request, field))) { crm_xml_add(request, field, user); } return requested_user; } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include #include bool xml_acl_enabled(const xmlNode *xml) { if (xml && xml->doc && xml->doc->_private){ xml_doc_private_t *docpriv = xml->doc->_private; return pcmk_is_set(docpriv->flags, pcmk__xf_acl_enabled); } return false; } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/logging.c b/lib/common/logging.c index f7af65910a..a879a32c27 100644 --- a/lib/common/logging.c +++ b/lib/common/logging.c @@ -1,1320 +1,1313 @@ /* * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Use high-resolution (millisecond) timestamps if libqb supports them #ifdef QB_FEATURE_LOG_HIRES_TIMESTAMPS #define TIMESTAMP_FORMAT_SPEC "%%T" typedef struct timespec *log_time_t; #else #define TIMESTAMP_FORMAT_SPEC "%%t" typedef time_t log_time_t; #endif unsigned int crm_log_level = LOG_INFO; unsigned int crm_trace_nonlog = 0; bool pcmk__is_daemon = false; static unsigned int crm_log_priority = LOG_NOTICE; static guint pcmk__log_id = 0; static guint pcmk__glib_log_id = 0; static guint pcmk__gio_log_id = 0; static guint pcmk__gmodule_log_id = 0; static guint pcmk__gthread_log_id = 0; static pcmk__output_t *logger_out = NULL; pcmk__config_error_func pcmk__config_error_handler = NULL; pcmk__config_warning_func pcmk__config_warning_handler = NULL; void *pcmk__config_error_context = NULL; void *pcmk__config_warning_context = NULL; static gboolean crm_tracing_enabled(void); static void crm_glib_handler(const gchar * log_domain, GLogLevelFlags flags, const gchar * message, gpointer user_data) { int log_level = LOG_WARNING; GLogLevelFlags msg_level = (flags & G_LOG_LEVEL_MASK); static struct qb_log_callsite *glib_cs = NULL; if (glib_cs == NULL) { glib_cs = qb_log_callsite_get(__func__, __FILE__, "glib-handler", LOG_DEBUG, __LINE__, crm_trace_nonlog); } switch (msg_level) { case G_LOG_LEVEL_CRITICAL: log_level = LOG_CRIT; if (!crm_is_callsite_active(glib_cs, LOG_DEBUG, crm_trace_nonlog)) { /* log and record how we got here */ crm_abort(__FILE__, __func__, __LINE__, message, TRUE, TRUE); } break; case G_LOG_LEVEL_ERROR: log_level = LOG_ERR; break; case G_LOG_LEVEL_MESSAGE: log_level = LOG_NOTICE; break; case G_LOG_LEVEL_INFO: log_level = LOG_INFO; break; case G_LOG_LEVEL_DEBUG: log_level = LOG_DEBUG; break; case G_LOG_LEVEL_WARNING: case G_LOG_FLAG_RECURSION: case G_LOG_FLAG_FATAL: case G_LOG_LEVEL_MASK: log_level = LOG_WARNING; break; } do_crm_log(log_level, "%s: %s", log_domain, message); } #ifndef NAME_MAX # define NAME_MAX 256 #endif /*! * \internal * \brief Write out a blackbox (enabling blackboxes if needed) * * \param[in] nsig Signal number that was received * * \note This is a true signal handler, and so must be async-safe. */ static void crm_trigger_blackbox(int nsig) { if(nsig == SIGTRAP) { /* Turn it on if it wasn't already */ crm_enable_blackbox(nsig); } crm_write_blackbox(nsig, NULL); } void crm_log_deinit(void) { if (pcmk__log_id == 0) { return; } g_log_remove_handler(G_LOG_DOMAIN, pcmk__log_id); pcmk__log_id = 0; g_log_remove_handler("GLib", pcmk__glib_log_id); pcmk__glib_log_id = 0; g_log_remove_handler("GLib-GIO", pcmk__gio_log_id); pcmk__gio_log_id = 0; g_log_remove_handler("GModule", pcmk__gmodule_log_id); pcmk__gmodule_log_id = 0; g_log_remove_handler("GThread", pcmk__gthread_log_id); pcmk__gthread_log_id = 0; } /*! * \internal * \brief Set the log format string based on the passed-in method * * \param[in] method The detail level of the log output * \param[in] daemon The daemon ID included in error messages * \param[in] use_pid Cached result of getpid() call, for efficiency * \param[in] use_nodename Cached result of uname() call, for efficiency * */ /* XXX __attribute__((nonnull)) for use_nodename parameter */ static void set_format_string(int method, const char *daemon, pid_t use_pid, const char *use_nodename) { if (method == QB_LOG_SYSLOG) { // The system log gets a simplified, user-friendly format qb_log_ctl(method, QB_LOG_CONF_EXTENDED, QB_FALSE); qb_log_format_set(method, "%g %p: %b"); } else { // Everything else gets more detail, for advanced troubleshooting GString *fmt = g_string_sized_new(256); if (method > QB_LOG_STDERR) { // If logging to file, prefix with timestamp, node name, daemon ID g_string_append_printf(fmt, TIMESTAMP_FORMAT_SPEC " %s %-20s[%lld] ", use_nodename, daemon, (long long) use_pid); } // Add function name (in parentheses) g_string_append(fmt, "(%n"); if (crm_tracing_enabled()) { // When tracing, add file and line number g_string_append(fmt, "@%f:%l"); } g_string_append_c(fmt, ')'); // Add tag (if any), severity, and actual message g_string_append(fmt, " %g\t%p: %b"); CRM_LOG_ASSERT(fmt->len > 0); qb_log_format_set(method, fmt->str); g_string_free(fmt, TRUE); } } #define DEFAULT_LOG_FILE CRM_LOG_DIR "/pacemaker.log" static bool logfile_disabled(const char *filename) { return pcmk__str_eq(filename, PCMK_VALUE_NONE, pcmk__str_casei) || pcmk__str_eq(filename, "/dev/null", pcmk__str_none); } /*! * \internal * \brief Fix log file ownership if group is wrong or doesn't have access * * \param[in] filename Log file name (for logging only) * \param[in] logfd Log file descriptor * * \return Standard Pacemaker return code */ static int chown_logfile(const char *filename, int logfd) { uid_t pcmk_uid = 0; gid_t pcmk_gid = 0; struct stat st; int rc; // Get the log file's current ownership and permissions if (fstat(logfd, &st) < 0) { return errno; } // Any other errors don't prevent file from being used as log rc = pcmk_daemon_user(&pcmk_uid, &pcmk_gid); if (rc != pcmk_ok) { rc = pcmk_legacy2rc(rc); crm_warn("Not changing '%s' ownership because user information " "unavailable: %s", filename, pcmk_rc_str(rc)); return pcmk_rc_ok; } if ((st.st_gid == pcmk_gid) && ((st.st_mode & S_IRWXG) == (S_IRGRP|S_IWGRP))) { return pcmk_rc_ok; } if (fchown(logfd, pcmk_uid, pcmk_gid) < 0) { crm_warn("Couldn't change '%s' ownership to user %s gid %d: %s", filename, CRM_DAEMON_USER, pcmk_gid, strerror(errno)); } return pcmk_rc_ok; } // Reset log file permissions (using environment variable if set) static void chmod_logfile(const char *filename, int logfd) { const char *modestr = pcmk__env_option(PCMK__ENV_LOGFILE_MODE); mode_t filemode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP; if (modestr != NULL) { long filemode_l = strtol(modestr, NULL, 8); if ((filemode_l != LONG_MIN) && (filemode_l != LONG_MAX)) { filemode = (mode_t) filemode_l; } } if ((filemode != 0) && (fchmod(logfd, filemode) < 0)) { crm_warn("Couldn't change '%s' mode to %04o: %s", filename, filemode, strerror(errno)); } } // If we're root, correct a log file's permissions if needed static int set_logfile_permissions(const char *filename, FILE *logfile) { if (geteuid() == 0) { int logfd = fileno(logfile); int rc = chown_logfile(filename, logfd); if (rc != pcmk_rc_ok) { return rc; } chmod_logfile(filename, logfd); } return pcmk_rc_ok; } // Enable libqb logging to a new log file static void enable_logfile(int fd) { qb_log_ctl(fd, QB_LOG_CONF_ENABLED, QB_TRUE); #if 0 qb_log_ctl(fd, QB_LOG_CONF_FILE_SYNC, 1); // Turn on synchronous writes #endif #ifdef HAVE_qb_log_conf_QB_LOG_CONF_MAX_LINE_LEN // Longer than default, for logging long XML lines qb_log_ctl(fd, QB_LOG_CONF_MAX_LINE_LEN, 800); #endif crm_update_callsites(); } static inline void disable_logfile(int fd) { qb_log_ctl(fd, QB_LOG_CONF_ENABLED, QB_FALSE); } static void setenv_logfile(const char *filename) { // Some resource agents will log only if environment variable is set if (pcmk__env_option(PCMK__ENV_LOGFILE) == NULL) { pcmk__set_env_option(PCMK__ENV_LOGFILE, filename, true); } } /*! * \brief Add a file to be used as a Pacemaker detail log * * \param[in] filename Name of log file to use * * \return Standard Pacemaker return code */ int pcmk__add_logfile(const char *filename) { /* No log messages from this function will be logged to the new log! * If another target such as syslog has already been added, the messages * should show up there. */ int fd = 0; int rc = pcmk_rc_ok; FILE *logfile = NULL; bool is_default = false; static int default_fd = -1; static bool have_logfile = false; // Use default if caller didn't specify (and we don't already have one) if (filename == NULL) { if (have_logfile) { return pcmk_rc_ok; } filename = DEFAULT_LOG_FILE; } // If the user doesn't want logging, we're done if (logfile_disabled(filename)) { return pcmk_rc_ok; } // If the caller wants the default and we already have it, we're done is_default = pcmk__str_eq(filename, DEFAULT_LOG_FILE, pcmk__str_none); if (is_default && (default_fd >= 0)) { return pcmk_rc_ok; } // Check whether we have write access to the file logfile = fopen(filename, "a"); if (logfile == NULL) { rc = errno; crm_warn("Logging to '%s' is disabled: %s " QB_XS " uid=%u gid=%u", filename, strerror(rc), geteuid(), getegid()); return rc; } rc = set_logfile_permissions(filename, logfile); if (rc != pcmk_rc_ok) { crm_warn("Logging to '%s' is disabled: %s " QB_XS " permissions", filename, strerror(rc)); fclose(logfile); return rc; } // Close and reopen as libqb logging target fclose(logfile); fd = qb_log_file_open(filename); if (fd < 0) { crm_warn("Logging to '%s' is disabled: %s " QB_XS " qb_log_file_open", filename, strerror(-fd)); return -fd; // == +errno } if (is_default) { default_fd = fd; setenv_logfile(filename); } else if (default_fd >= 0) { crm_notice("Switching logging to %s", filename); disable_logfile(default_fd); } crm_notice("Additional logging available in %s", filename); enable_logfile(fd); have_logfile = true; return pcmk_rc_ok; } /*! * \brief Add multiple additional log files * * \param[in] log_files Array of log files to add * \param[in] out Output object to use for error reporting * * \return Standard Pacemaker return code */ void pcmk__add_logfiles(gchar **log_files, pcmk__output_t *out) { if (log_files == NULL) { return; } for (gchar **fname = log_files; *fname != NULL; fname++) { int rc = pcmk__add_logfile(*fname); if (rc != pcmk_rc_ok) { out->err(out, "Logging to %s is disabled: %s", *fname, pcmk_rc_str(rc)); } } } static int blackbox_trigger = 0; static volatile char *blackbox_file_prefix = NULL; static void blackbox_logger(int32_t t, struct qb_log_callsite *cs, log_time_t timestamp, const char *msg) { if(cs && cs->priority < LOG_ERR) { crm_write_blackbox(SIGTRAP, cs); /* Bypass the over-dumping logic */ } else { crm_write_blackbox(0, cs); } } static void crm_control_blackbox(int nsig, bool enable) { int lpc = 0; if (blackbox_file_prefix == NULL) { pid_t pid = getpid(); blackbox_file_prefix = crm_strdup_printf("%s/%s-%lu", CRM_BLACKBOX_DIR, crm_system_name, (unsigned long) pid); } if (enable && qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_SIZE, 5 * 1024 * 1024); /* Any size change drops existing entries */ qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE); /* Setting the size seems to disable it */ /* Enable synchronous logging */ for (lpc = QB_LOG_BLACKBOX; lpc < QB_LOG_TARGET_MAX; lpc++) { qb_log_ctl(lpc, QB_LOG_CONF_FILE_SYNC, QB_TRUE); } crm_notice("Initiated blackbox recorder: %s", blackbox_file_prefix); /* Save to disk on abnormal termination */ crm_signal_handler(SIGSEGV, crm_trigger_blackbox); crm_signal_handler(SIGABRT, crm_trigger_blackbox); crm_signal_handler(SIGILL, crm_trigger_blackbox); crm_signal_handler(SIGBUS, crm_trigger_blackbox); crm_signal_handler(SIGFPE, crm_trigger_blackbox); crm_update_callsites(); blackbox_trigger = qb_log_custom_open(blackbox_logger, NULL, NULL, NULL); qb_log_ctl(blackbox_trigger, QB_LOG_CONF_ENABLED, QB_TRUE); crm_trace("Trigger: %d is %d %d", blackbox_trigger, qb_log_ctl(blackbox_trigger, QB_LOG_CONF_STATE_GET, 0), QB_LOG_STATE_ENABLED); crm_update_callsites(); } else if (!enable && qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0) == QB_LOG_STATE_ENABLED) { qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); /* Disable synchronous logging again when the blackbox is disabled */ for (lpc = QB_LOG_BLACKBOX; lpc < QB_LOG_TARGET_MAX; lpc++) { qb_log_ctl(lpc, QB_LOG_CONF_FILE_SYNC, QB_FALSE); } } } void crm_enable_blackbox(int nsig) { crm_control_blackbox(nsig, TRUE); } void crm_disable_blackbox(int nsig) { crm_control_blackbox(nsig, FALSE); } /*! * \internal * \brief Write out a blackbox, if blackboxes are enabled * * \param[in] nsig Signal that was received * \param[in] cs libqb callsite * * \note This may be called via a true signal handler and so must be async-safe. * @TODO actually make this async-safe */ void crm_write_blackbox(int nsig, const struct qb_log_callsite *cs) { static volatile int counter = 1; static volatile time_t last = 0; char *buffer = NULL; int rc = 0; time_t now = time(NULL); if (blackbox_file_prefix == NULL) { return; } switch (nsig) { case 0: case SIGTRAP: /* The graceful case - such as assertion failure or user request */ if (nsig == 0 && now == last) { /* Prevent over-dumping */ return; } buffer = crm_strdup_printf("%s.%d", blackbox_file_prefix, counter++); if (nsig == SIGTRAP) { crm_notice("Blackbox dump requested, please see %s for contents", buffer); } else if (cs) { syslog(LOG_NOTICE, "Problem detected at %s:%d (%s), please see %s for additional details", cs->function, cs->lineno, cs->filename, buffer); } else { crm_notice("Problem detected, please see %s for additional details", buffer); } last = now; rc = qb_log_blackbox_write_to_file(buffer); if (rc < 0) { // System errno crm_err("Failed to write blackbox file %s: %s", buffer, strerror(-rc)); } /* Flush the existing contents * A size change would also work */ qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE); break; default: /* Do as little as possible, just try to get what we have out * We logged the filename when the blackbox was enabled */ crm_signal_handler(nsig, SIG_DFL); qb_log_blackbox_write_to_file((const char *)blackbox_file_prefix); qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); raise(nsig); break; } free(buffer); } static const char * crm_quark_to_string(uint32_t tag) { const char *text = g_quark_to_string(tag); if (text) { return text; } return ""; } static void crm_log_filter_source(int source, const char *trace_files, const char *trace_fns, const char *trace_fmts, const char *trace_tags, const char *trace_blackbox, struct qb_log_callsite *cs) { if (qb_log_ctl(source, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { return; } else if (cs->tags != crm_trace_nonlog && source == QB_LOG_BLACKBOX) { /* Blackbox gets everything if enabled */ qb_bit_set(cs->targets, source); } else if (source == blackbox_trigger && blackbox_trigger > 0) { /* Should this log message result in the blackbox being dumped */ if (cs->priority <= LOG_ERR) { qb_bit_set(cs->targets, source); } else if (trace_blackbox) { char *key = crm_strdup_printf("%s:%d", cs->function, cs->lineno); if (strstr(trace_blackbox, key) != NULL) { qb_bit_set(cs->targets, source); } free(key); } } else if (source == QB_LOG_SYSLOG) { /* No tracing to syslog */ if (cs->priority <= crm_log_priority && cs->priority <= crm_log_level) { qb_bit_set(cs->targets, source); } /* Log file tracing options... */ } else if (cs->priority <= crm_log_level) { qb_bit_set(cs->targets, source); } else if (trace_files && strstr(trace_files, cs->filename) != NULL) { qb_bit_set(cs->targets, source); } else if (trace_fns && strstr(trace_fns, cs->function) != NULL) { qb_bit_set(cs->targets, source); } else if (trace_fmts && strstr(trace_fmts, cs->format) != NULL) { qb_bit_set(cs->targets, source); } else if (trace_tags && cs->tags != 0 && cs->tags != crm_trace_nonlog && g_quark_to_string(cs->tags) != NULL) { qb_bit_set(cs->targets, source); } } #ifndef HAVE_STRCHRNUL /* strchrnul() is a GNU extension. If not present, use our own definition. * The GNU version returns char*, but we only need it to be const char*. */ static const char * strchrnul(const char *s, int c) { while ((*s != c) && (*s != '\0')) { ++s; } return s; } #endif static void crm_log_filter(struct qb_log_callsite *cs) { int lpc = 0; static int need_init = 1; static const char *trace_fns = NULL; static const char *trace_tags = NULL; static const char *trace_fmts = NULL; static const char *trace_files = NULL; static const char *trace_blackbox = NULL; if (need_init) { need_init = 0; trace_fns = pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS); trace_fmts = pcmk__env_option(PCMK__ENV_TRACE_FORMATS); trace_tags = pcmk__env_option(PCMK__ENV_TRACE_TAGS); trace_files = pcmk__env_option(PCMK__ENV_TRACE_FILES); trace_blackbox = pcmk__env_option(PCMK__ENV_TRACE_BLACKBOX); if (trace_tags != NULL) { uint32_t tag; const char *offset = NULL; const char *next = trace_tags; // @TODO Use g_strsplit() to simplify do { char *token = NULL; offset = next; next = strchrnul(offset, ','); token = crm_strdup_printf("%.*s", (int) (next - offset), offset); tag = g_quark_from_string(token); crm_info("Created GQuark %u from token '%s' in '%s'", tag, token, trace_tags); free(token); if (next[0] != 0) { next++; } } while (next != NULL && next[0] != 0); } } cs->targets = 0; /* Reset then find targets to enable */ for (lpc = QB_LOG_SYSLOG; lpc < QB_LOG_TARGET_MAX; lpc++) { crm_log_filter_source(lpc, trace_files, trace_fns, trace_fmts, trace_tags, trace_blackbox, cs); } } gboolean crm_is_callsite_active(struct qb_log_callsite *cs, uint8_t level, uint32_t tags) { gboolean refilter = FALSE; if (cs == NULL) { return FALSE; } if (cs->priority != level) { cs->priority = level; refilter = TRUE; } if (cs->tags != tags) { cs->tags = tags; refilter = TRUE; } if (refilter) { crm_log_filter(cs); } if (cs->targets == 0) { return FALSE; } return TRUE; } void crm_update_callsites(void) { static gboolean log = TRUE; if (log) { log = FALSE; crm_debug ("Enabling callsites based on priority=%d, files=%s, functions=%s, formats=%s, tags=%s", crm_log_level, pcmk__env_option(PCMK__ENV_TRACE_FILES), pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS), pcmk__env_option(PCMK__ENV_TRACE_FORMATS), pcmk__env_option(PCMK__ENV_TRACE_TAGS)); } qb_log_filter_fn_set(crm_log_filter); } static gboolean crm_tracing_enabled(void) { return (crm_log_level == LOG_TRACE) || (pcmk__env_option(PCMK__ENV_TRACE_FILES) != NULL) || (pcmk__env_option(PCMK__ENV_TRACE_FUNCTIONS) != NULL) || (pcmk__env_option(PCMK__ENV_TRACE_FORMATS) != NULL) || (pcmk__env_option(PCMK__ENV_TRACE_TAGS) != NULL); } static int crm_priority2int(const char *name) { struct syslog_names { const char *name; int priority; }; static struct syslog_names p_names[] = { {"emerg", LOG_EMERG}, {"alert", LOG_ALERT}, {"crit", LOG_CRIT}, {"error", LOG_ERR}, {"warning", LOG_WARNING}, {"notice", LOG_NOTICE}, {"info", LOG_INFO}, {"debug", LOG_DEBUG}, {NULL, -1} }; int lpc; for (lpc = 0; name != NULL && p_names[lpc].name != NULL; lpc++) { if (pcmk__str_eq(p_names[lpc].name, name, pcmk__str_none)) { return p_names[lpc].priority; } } return crm_log_priority; } /*! * \internal * \brief Set the identifier for the current process * * If the identifier crm_system_name is not already set, then it is set as follows: * - it is passed to the function via the "entity" parameter, or * - it is derived from the executable name * * The identifier can be used in logs, IPC, and more. * * This method also sets the PCMK_service environment variable. * * \param[in] entity If not NULL, will be assigned to the identifier * \param[in] argc The number of command line parameters * \param[in] argv The command line parameter values */ static void set_identity(const char *entity, int argc, char *const *argv) { if (crm_system_name != NULL) { return; // Already set, don't overwrite } if (entity != NULL) { crm_system_name = pcmk__str_copy(entity); } else if ((argc > 0) && (argv != NULL)) { char *mutable = strdup(argv[0]); char *modified = basename(mutable); if (strstr(modified, "lt-") == modified) { modified += 3; } crm_system_name = pcmk__str_copy(modified); free(mutable); } else { crm_system_name = pcmk__str_copy("Unknown"); } // Used by fencing.py.py (in fence-agents) pcmk__set_env_option(PCMK__ENV_SERVICE, crm_system_name, false); } void crm_log_preinit(const char *entity, int argc, char *const *argv) { /* Configure libqb logging with nothing turned on */ struct utsname res; int lpc = 0; int32_t qb_facility = 0; pid_t pid = getpid(); const char *nodename = "localhost"; static bool have_logging = false; GLogLevelFlags log_levels; if (have_logging) { return; } have_logging = true; /* @TODO Try to create a more obvious "global Pacemaker initializer" * function than crm_log_preinit(), and call pcmk__schema_init() there. * See also https://projects.clusterlabs.org/T840. */ pcmk__schema_init(); if (crm_trace_nonlog == 0) { crm_trace_nonlog = g_quark_from_static_string("Pacemaker non-logging tracepoint"); } umask(S_IWGRP | S_IWOTH | S_IROTH); /* Add a log handler for messages from our log domain at any log level. */ log_levels = G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION; pcmk__log_id = g_log_set_handler(G_LOG_DOMAIN, log_levels, crm_glib_handler, NULL); /* Add a log handler for messages from the GLib domains at any log level. */ pcmk__glib_log_id = g_log_set_handler("GLib", log_levels, crm_glib_handler, NULL); pcmk__gio_log_id = g_log_set_handler("GLib-GIO", log_levels, crm_glib_handler, NULL); pcmk__gmodule_log_id = g_log_set_handler("GModule", log_levels, crm_glib_handler, NULL); pcmk__gthread_log_id = g_log_set_handler("GThread", log_levels, crm_glib_handler, NULL); /* glib should not abort for any messages from the Pacemaker domain, but * other domains are still free to specify their own behavior. However, * note that G_LOG_LEVEL_ERROR is always fatal regardless of what we do * here. */ g_log_set_fatal_mask(G_LOG_DOMAIN, 0); /* Set crm_system_name, which is used as the logging name. It may also * be used for other purposes such as an IPC client name. */ set_identity(entity, argc, argv); qb_facility = qb_log_facility2int("local0"); qb_log_init(crm_system_name, qb_facility, LOG_ERR); crm_log_level = LOG_CRIT; /* Nuke any syslog activity until it's asked for */ qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_ENABLED, QB_FALSE); #ifdef HAVE_qb_log_conf_QB_LOG_CONF_MAX_LINE_LEN // Shorter than default, generous for what we *should* send to syslog qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_MAX_LINE_LEN, 256); #endif if (uname(memset(&res, 0, sizeof(res))) == 0 && *res.nodename != '\0') { nodename = res.nodename; } /* Set format strings and disable threading * Pacemaker and threads do not mix well (due to the amount of forking) */ qb_log_tags_stringify_fn_set(crm_quark_to_string); for (lpc = QB_LOG_SYSLOG; lpc < QB_LOG_TARGET_MAX; lpc++) { qb_log_ctl(lpc, QB_LOG_CONF_THREADED, QB_FALSE); #ifdef HAVE_qb_log_conf_QB_LOG_CONF_ELLIPSIS // End truncated lines with '...' qb_log_ctl(lpc, QB_LOG_CONF_ELLIPSIS, QB_TRUE); #endif set_format_string(lpc, crm_system_name, pid, nodename); } #ifdef ENABLE_NLS /* Enable translations (experimental). Currently we only have a few * proof-of-concept translations for some option help. The goal would be to * offer translations for option help and man pages rather than logs or * documentation, to reduce the burden of maintaining them. */ // Load locale information for the local host from the environment setlocale(LC_ALL, ""); // Tell gettext where to find Pacemaker message catalogs pcmk__assert(bindtextdomain(PACKAGE, PCMK__LOCALE_DIR) != NULL); // Tell gettext to use the Pacemaker message catalogs pcmk__assert(textdomain(PACKAGE) != NULL); // Tell gettext that the translated strings are stored in UTF-8 bind_textdomain_codeset(PACKAGE, "UTF-8"); #endif } gboolean crm_log_init(const char *entity, uint8_t level, gboolean daemon, gboolean to_stderr, int argc, char **argv, gboolean quiet) { const char *syslog_priority = NULL; const char *facility = pcmk__env_option(PCMK__ENV_LOGFACILITY); const char *f_copy = facility; pcmk__is_daemon = daemon; crm_log_preinit(entity, argc, argv); if (level > LOG_TRACE) { level = LOG_TRACE; } if(level > crm_log_level) { crm_log_level = level; } /* Should we log to syslog */ if (facility == NULL) { if (pcmk__is_daemon) { facility = "daemon"; } else { facility = PCMK_VALUE_NONE; } pcmk__set_env_option(PCMK__ENV_LOGFACILITY, facility, true); } if (pcmk__str_eq(facility, PCMK_VALUE_NONE, pcmk__str_casei)) { quiet = TRUE; } else { qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_FACILITY, qb_log_facility2int(facility)); } if (pcmk__env_option_enabled(crm_system_name, PCMK__ENV_DEBUG)) { /* Override the default setting */ crm_log_level = LOG_DEBUG; } /* What lower threshold do we have for sending to syslog */ syslog_priority = pcmk__env_option(PCMK__ENV_LOGPRIORITY); if (syslog_priority) { crm_log_priority = crm_priority2int(syslog_priority); } qb_log_filter_ctl(QB_LOG_SYSLOG, QB_LOG_FILTER_ADD, QB_LOG_FILTER_FILE, "*", crm_log_priority); // Log to syslog unless requested to be quiet if (!quiet) { qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_ENABLED, QB_TRUE); } /* Should we log to stderr */ if (pcmk__env_option_enabled(crm_system_name, PCMK__ENV_STDERR)) { /* Override the default setting */ to_stderr = TRUE; } crm_enable_stderr(to_stderr); // Log to a file if we're a daemon or user asked for one { const char *logfile = pcmk__env_option(PCMK__ENV_LOGFILE); if (!pcmk__str_eq(PCMK_VALUE_NONE, logfile, pcmk__str_casei) && (pcmk__is_daemon || (logfile != NULL))) { // Daemons always get a log file, unless explicitly set to "none" pcmk__add_logfile(logfile); } } if (pcmk__is_daemon && pcmk__env_option_enabled(crm_system_name, PCMK__ENV_BLACKBOX)) { crm_enable_blackbox(0); } /* Summary */ crm_trace("Quiet: %d, facility %s", quiet, f_copy); pcmk__env_option(PCMK__ENV_LOGFILE); pcmk__env_option(PCMK__ENV_LOGFACILITY); crm_update_callsites(); /* Ok, now we can start logging... */ // Disable daemon request if user isn't root or Pacemaker daemon user if (pcmk__is_daemon) { const char *user = getenv("USER"); if (user != NULL && !pcmk__strcase_any_of(user, "root", CRM_DAEMON_USER, NULL)) { crm_trace("Not switching to corefile directory for %s", user); pcmk__is_daemon = false; } } if (pcmk__is_daemon) { - uid_t user = getuid(); - struct passwd *pwent = NULL; + char *user = pcmk__uid2username(getuid()); - errno = 0; - pwent = getpwuid(user); + if (user == NULL) { + // Error already logged - if (pwent == NULL) { - const char *reason = "No matching password database entry found"; - - if (errno != 0) { - reason = strerror(errno); - } - crm_err("Cannot get name for uid %lld: %s", (long long) user, - reason); - - } else if (!pcmk__strcase_any_of(pwent->pw_name, "root", CRM_DAEMON_USER, NULL)) { - crm_trace("Don't change active directory for regular user: %s", pwent->pw_name); + } else if (!pcmk__str_any_of(user, "root", CRM_DAEMON_USER, NULL)) { + crm_trace("Don't change active directory for regular user %s", + user); } else if (chdir(CRM_CORE_DIR) < 0) { crm_info("Cannot change active directory to " CRM_CORE_DIR ": %s", strerror(errno)); } else { crm_info("Changed active directory to " CRM_CORE_DIR); } /* Original meanings from signal(7) * * Signal Value Action Comment * SIGTRAP 5 Core Trace/breakpoint trap * SIGUSR1 30,10,16 Term User-defined signal 1 * SIGUSR2 31,12,17 Term User-defined signal 2 * * Our usage is as similar as possible */ mainloop_add_signal(SIGUSR1, crm_enable_blackbox); mainloop_add_signal(SIGUSR2, crm_disable_blackbox); mainloop_add_signal(SIGTRAP, crm_trigger_blackbox); + free(user); + } else if (!quiet) { crm_log_args(argc, argv); } return TRUE; } /* returns the old value */ unsigned int set_crm_log_level(unsigned int level) { unsigned int old = crm_log_level; if (level > LOG_TRACE) { level = LOG_TRACE; } crm_log_level = level; crm_update_callsites(); crm_trace("New log level: %d", level); return old; } void crm_enable_stderr(int enable) { if (enable && qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_ENABLED, QB_TRUE); crm_update_callsites(); } else if (enable == FALSE) { qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_ENABLED, QB_FALSE); } } /*! * \brief Make logging more verbose * * If logging to stderr is not already enabled when this function is called, * enable it. Otherwise, increase the log level by 1. * * \param[in] argc Ignored * \param[in] argv Ignored */ void crm_bump_log_level(int argc, char **argv) { if (qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { crm_enable_stderr(TRUE); } else { set_crm_log_level(crm_log_level + 1); } } unsigned int get_crm_log_level(void) { return crm_log_level; } /*! * \brief Log the command line (once) * * \param[in] Number of values in \p argv * \param[in] Command-line arguments (including command name) * * \note This function will only log once, even if called with different * arguments. */ void crm_log_args(int argc, char **argv) { static bool logged = false; gchar *arg_string = NULL; if ((argc == 0) || (argv == NULL) || logged) { return; } logged = true; arg_string = g_strjoinv(" ", argv); crm_notice("Invoked: %s", arg_string); g_free(arg_string); } void crm_log_output_fn(const char *file, const char *function, int line, int level, const char *prefix, const char *output) { const char *next = NULL; const char *offset = NULL; if (level == LOG_NEVER) { return; } if (output == NULL) { if (level != LOG_STDOUT) { level = LOG_TRACE; } output = "-- empty --"; } next = output; do { offset = next; next = strchrnul(offset, '\n'); do_crm_log_alias(level, file, function, line, "%s [ %.*s ]", prefix, (int)(next - offset), offset); if (next[0] != 0) { next++; } } while (next != NULL && next[0] != 0); } void pcmk__cli_init_logging(const char *name, unsigned int verbosity) { crm_log_init(name, LOG_ERR, FALSE, FALSE, 0, NULL, TRUE); for (int i = 0; i < verbosity; i++) { /* These arguments are ignored, so pass placeholders. */ crm_bump_log_level(0, NULL); } } /*! * \brief Log XML line-by-line in a formatted fashion * * \param[in] file File name to use for log filtering * \param[in] function Function name to use for log filtering * \param[in] line Line number to use for log filtering * \param[in] tags Logging tags to use for log filtering * \param[in] level Priority at which to log the messages * \param[in] text Prefix for each line * \param[in] xml XML to log * * \note This does nothing when \p level is \p LOG_STDOUT. * \note Do not call this function directly. It should be called only from the * \p do_crm_log_xml() macro. */ void pcmk_log_xml_as(const char *file, const char *function, uint32_t line, uint32_t tags, uint8_t level, const char *text, const xmlNode *xml) { if (xml == NULL) { do_crm_log(level, "%s%sNo data to dump as XML", pcmk__s(text, ""), pcmk__str_empty(text)? "" : " "); } else { if (logger_out == NULL) { CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return); } pcmk__output_set_log_level(logger_out, level); pcmk__output_set_log_filter(logger_out, file, function, line, tags); pcmk__xml_show(logger_out, text, xml, 1, pcmk__xml_fmt_pretty |pcmk__xml_fmt_open |pcmk__xml_fmt_children |pcmk__xml_fmt_close); pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U); } } /*! * \internal * \brief Log XML changes line-by-line in a formatted fashion * * \param[in] file File name to use for log filtering * \param[in] function Function name to use for log filtering * \param[in] line Line number to use for log filtering * \param[in] tags Logging tags to use for log filtering * \param[in] level Priority at which to log the messages * \param[in] xml XML whose changes to log * * \note This does nothing when \p level is \c LOG_STDOUT. */ void pcmk__log_xml_changes_as(const char *file, const char *function, uint32_t line, uint32_t tags, uint8_t level, const xmlNode *xml) { if (xml == NULL) { do_crm_log(level, "No XML to dump"); return; } if (logger_out == NULL) { CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return); } pcmk__output_set_log_level(logger_out, level); pcmk__output_set_log_filter(logger_out, file, function, line, tags); pcmk__xml_show_changes(logger_out, xml); pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U); } /*! * \internal * \brief Log an XML patchset line-by-line in a formatted fashion * * \param[in] file File name to use for log filtering * \param[in] function Function name to use for log filtering * \param[in] line Line number to use for log filtering * \param[in] tags Logging tags to use for log filtering * \param[in] level Priority at which to log the messages * \param[in] patchset XML patchset to log * * \note This does nothing when \p level is \c LOG_STDOUT. */ void pcmk__log_xml_patchset_as(const char *file, const char *function, uint32_t line, uint32_t tags, uint8_t level, const xmlNode *patchset) { if (patchset == NULL) { do_crm_log(level, "No patchset to dump"); return; } if (logger_out == NULL) { CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return); } pcmk__output_set_log_level(logger_out, level); pcmk__output_set_log_filter(logger_out, file, function, line, tags); logger_out->message(logger_out, "xml-patchset", patchset); pcmk__output_set_log_filter(logger_out, NULL, NULL, 0U, 0U); } /*! * \internal * \brief Free the logging library's internal log output object */ void pcmk__free_common_logger(void) { if (logger_out != NULL) { logger_out->finish(logger_out, CRM_EX_OK, true, NULL); pcmk__output_free(logger_out); logger_out = NULL; } } void pcmk__set_config_error_handler(pcmk__config_error_func error_handler, void *error_context) { pcmk__config_error_handler = error_handler; pcmk__config_error_context = error_context; } void pcmk__set_config_warning_handler(pcmk__config_warning_func warning_handler, void *warning_context) { pcmk__config_warning_handler = warning_handler; pcmk__config_warning_context = warning_context; } diff --git a/tools/cibadmin.c b/tools/cibadmin.c index 953ba1d8aa..83e67f4367 100644 --- a/tools/cibadmin.c +++ b/tools/cibadmin.c @@ -1,1250 +1,1258 @@ /* * Copyright 2004-2025 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include // uint32_t, etc. #include // NULL #include #include #include #include #include #include #define SUMMARY "query and edit the Pacemaker configuration" #define DEFAULT_TIMEOUT 30 #define INDENT " " /*! * \internal * \brief How to interpret \c options.cib_section */ enum cibadmin_section_type { //! No section specified: the command applies to the entire CIB cibadmin_section_all = 0, //! Section is the name of the CIB element to which the command applies cibadmin_section_scope, //! Section is an XPath expression, and the command applies to matches cibadmin_section_xpath, }; /*! * \internal * \brief Commands for \c cibadmin */ enum cibadmin_cmd { cibadmin_cmd_bump, cibadmin_cmd_create, cibadmin_cmd_delete, cibadmin_cmd_delete_all, cibadmin_cmd_empty, cibadmin_cmd_erase, cibadmin_cmd_md5_sum, cibadmin_cmd_md5_sum_versioned, cibadmin_cmd_modify, cibadmin_cmd_patch, cibadmin_cmd_query, cibadmin_cmd_replace, cibadmin_cmd_upgrade, // Update this when adding new commands cibadmin_cmd_max = cibadmin_cmd_upgrade, }; /*! * \internal * \brief Flags to define attributes of a given \c cibadmin command */ enum cibadmin_command_flags { //! This flag has no effect cibadmin_cf_none = UINT32_C(0), /*! * \brief Command requires input * * There is no optional input. Either a command requires input, or it * ignores any input that was provided. */ cibadmin_cf_requires_input = (UINT32_C(1) << 0), /*! * \brief Command is especially unsafe * * Any command that modifies the CIB is unsafe. This flag is for commands * that are likely to be destructive to larger portions of the CIB and to be * used by mistake. */ cibadmin_cf_unsafe = (UINT32_C(1) << 1), /*! * \brief Command can use an XPath expression instead of input XML * * If \c options.section_type is \c cibadmin_section_xpath, then the command * uses \c options.cib_section rather than reading input XML. */ cibadmin_cf_xpath_input = (UINT32_C(1) << 2), }; /*! * \internal * \brief Setup function for a \c cibadmin command (before any CIB API call) */ typedef crm_exit_t (*cibadmin_pre_fn_t)(pcmk__output_t *, int *, xmlNode *, GError **); /*! * \internal * \brief Return/output handler for a \c cibadmin command (after CIB API call) */ typedef crm_exit_t (*cibadmin_post_fn_t)(pcmk__output_t *, cib_t *, int, xmlNode *, int, GError **); /*! * \internal * \brief Information about a \c cibadmin command type */ typedef struct { const char *cib_request; //!< Name of request to send to the CIB API cibadmin_pre_fn_t pre_fn; //!< Function to call before CIB API call cibadmin_post_fn_t post_fn; //!< Function to call after CIB API call //! Group of enum cibadmin_command_flags uint32_t flags; } cibadmin_cmd_info_t; static struct { enum cibadmin_cmd cmd; enum cibadmin_section_type section_type; char *cib_section; char *validate_with; gint timeout_sec; enum pcmk__acl_render_how acl_render_mode; gchar *cib_user; gchar *input_file; gchar *input_string; gboolean input_stdin; gboolean allow_create; gboolean force; gboolean get_node_path; gboolean no_children; gboolean score_update; // @COMPAT Deprecated since 3.0.2 gchar *dest_node; // @COMPAT Deprecated since 3.0.0 gboolean local; // @COMPAT Deprecated since 3.0.1 gboolean sync_call; } options = { .cmd = cibadmin_cmd_query, .timeout_sec = DEFAULT_TIMEOUT, }; /*! * \internal * \brief Determine whether the given CIB scope is valid for \p cibadmin * * \param[in] scope Scope to validate * * \return true if \p scope is valid, or false otherwise * \note An invalid scope applies the operation to the entire CIB. */ static inline bool scope_is_valid(const char *scope) { return pcmk__str_any_of(scope, PCMK_XE_CONFIGURATION, PCMK_XE_NODES, PCMK_XE_RESOURCES, PCMK_XE_CONSTRAINTS, PCMK_XE_CRM_CONFIG, PCMK_XE_RSC_DEFAULTS, PCMK_XE_OP_DEFAULTS, PCMK_XE_ACLS, PCMK_XE_FENCING_TOPOLOGY, PCMK_XE_TAGS, PCMK_XE_ALERTS, PCMK_XE_STATUS, NULL); } static void cibadmin_output_basic_xml(pcmk__output_t *out, const xmlNode *xml) { GString *buf = g_string_sized_new(1024); pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buf, 0); out->output_xml(out, PCMK_XE_OUTPUT, buf->str); g_string_free(buf, TRUE); } static crm_exit_t cibadmin_pre_delete_all(pcmk__output_t *out, int *call_options, xmlNode *input, GError **error) { // Remove all matching objects. Meaningful only with cibadmin_section_xpath. cib__set_call_options(*call_options, crm_system_name, cib_multiple); return CRM_EX_OK; } static crm_exit_t cibadmin_pre_empty(pcmk__output_t *out, int *call_options, xmlNode *input, GError **error) { /* Output an empty CIB. * Handles entirety of empty command; there is no CIB request. */ xmlNode *output = createEmptyCib(1); crm_xml_add(output, PCMK_XA_VALIDATE_WITH, options.validate_with); cibadmin_output_basic_xml(out, output); pcmk__xml_free(output); return CRM_EX_OK; } static crm_exit_t cibadmin_pre_md5_sum(pcmk__output_t *out, int *call_options, xmlNode *input, GError **error) { // Handles entirety of md5_sum command; there is no CIB request int rc = pcmk_rc_ok; char *digest = pcmk__digest_on_disk_cib(input); if (digest == NULL) { /* On-disk digest should be non-NULL even if input is NULL or empty, * since whitespace gets added before and after dumping the XML */ g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_SOFTWARE, "Bug: Null digest"); return CRM_EX_SOFTWARE; } rc = out->message(out, "cibadmin-md5-sum", digest); free(digest); return pcmk_rc2exitc(rc); } static crm_exit_t cibadmin_pre_md5_sum_versioned(pcmk__output_t *out, int *call_options, xmlNode *input, GError **error) { // Handles entirety of md5_sum_versioned command; there is no CIB request int rc = pcmk_rc_ok; char *digest = pcmk__digest_xml(input, true); if (digest == NULL) { int rc = pcmk_rc_bad_input; g_set_error(error, PCMK__RC_ERROR, rc, "Couldn't compute digest: %s", pcmk_rc_str(rc)); return pcmk_rc2exitc(rc); } rc = out->message(out, "cibadmin-md5-sum", digest); free(digest); return pcmk_rc2exitc(rc); } static crm_exit_t cibadmin_pre_modify(pcmk__output_t *out, int *call_options, xmlNode *input, GError **error) { /* @COMPAT When we drop default support for expansion in cibadmin, guard * with `if (options.score_update)` */ cib__set_call_options(*call_options, crm_system_name, cib_score_update); if (options.allow_create) { // Allow target to be created if it does not exist cib__set_call_options(*call_options, crm_system_name, cib_can_create); } return CRM_EX_OK; } static crm_exit_t cibadmin_pre_query(pcmk__output_t *out, int *call_options, xmlNode *input, GError **error) { if (options.get_node_path) { /* Enable getting node path of XPath query matches. Meaningful only with * cibadmin_section_xpath. */ cib__set_call_options(*call_options, crm_system_name, cib_xpath_address); } if (options.no_children) { // Don't include a match's children in the query result cib__set_call_options(*call_options, crm_system_name, cib_no_children); } return CRM_EX_OK; } static crm_exit_t cibadmin_pre_replace(pcmk__output_t *out, int *call_options, xmlNode *input, GError **error) { if (pcmk__xe_is(input, PCMK_XE_CIB)) { xmlNode *status = pcmk_find_cib_element(input, PCMK_XE_STATUS); if (status == NULL) { pcmk__xe_create(input, PCMK_XE_STATUS); } } return CRM_EX_OK; } static crm_exit_t cibadmin_post_upgrade(pcmk__output_t *out, cib_t *cib_conn, int call_options, xmlNode *output, int cib_rc, GError **error) { if (cib_rc == pcmk_rc_ok) { return CRM_EX_OK; } if (cib_rc == pcmk_rc_schema_unchanged) { out->info(out, "Upgrade unnecessary: %s", pcmk_rc_str(cib_rc)); return CRM_EX_OK; } g_set_error(error, PCMK__RC_ERROR, cib_rc, "CIB API call failed: %s", pcmk_rc_str(cib_rc)); if (cib_rc == pcmk_rc_schema_validation) { xmlNode *obj = NULL; if (cib_conn->cmds->query(cib_conn, NULL, &obj, call_options) == pcmk_ok) { pcmk__update_schema(&obj, NULL, true, false); } pcmk__xml_free(obj); } return pcmk_rc2exitc(cib_rc); } static crm_exit_t cibadmin_post_default(pcmk__output_t *out, cib_t *cib_conn, int call_options, xmlNode *output, int cib_rc, GError **error) { if (cib_rc != pcmk_rc_ok) { g_set_error(error, PCMK__RC_ERROR, cib_rc, "CIB API call failed: %s", pcmk_rc_str(cib_rc)); if ((cib_rc == pcmk_rc_schema_validation) && pcmk__xe_is(output, PCMK_XE_CIB)) { // Show validation errors to stderr pcmk__validate_xml(output, NULL, NULL, NULL); } return pcmk_rc2exitc(cib_rc); } return CRM_EX_OK; } static void cibadmin_output_xml(pcmk__output_t *out, xmlNode *xml, int call_options, const gchar *acl_user, crm_exit_t *exit_code, GError **error) { if ((options.acl_render_mode != pcmk__acl_render_none) && (*exit_code == CRM_EX_OK) && pcmk__xe_is(xml, PCMK_XE_CIB)) { xmlDoc *acl_evaled_doc = NULL; xmlChar *rendered = NULL; int rc = pcmk__acl_annotate_permissions(acl_user, xml->doc, &acl_evaled_doc); if (rc != pcmk_rc_ok) { *exit_code = CRM_EX_CONFIG; g_set_error(error, PCMK__EXITC_ERROR, *exit_code, "Could not evaluate ACLs for %s: %s", acl_user, pcmk_rc_str(rc)); return; } rc = pcmk__acl_evaled_render(acl_evaled_doc, options.acl_render_mode, &rendered); if (rc != pcmk_rc_ok) { *exit_code = CRM_EX_CONFIG; g_set_error(error, PCMK__EXITC_ERROR, *exit_code, "Could not render ACLs for %s: %s", acl_user, pcmk_rc_str(rc)); return; } out->message(out, "cibadmin-rendered-acls", (const char *) rendered); xmlFree(rendered); } else if (pcmk_is_set(call_options, cib_xpath_address) && pcmk__xe_is(xml, PCMK__XE_XPATH_QUERY)) { // @COMPAT Remove when -e/--node-path is removed out->message(out, "cibadmin-node-path", xml); } else { cibadmin_output_basic_xml(out, xml); } } static crm_exit_t cibadmin_handle_command(pcmk__output_t *out, const cibadmin_cmd_info_t *cmd_info, int call_options, const gchar *acl_user, xmlNode *input, GError **error) { int rc = pcmk_rc_ok; crm_exit_t exit_code = CRM_EX_OK; cib_t *cib_conn = NULL; xmlNode *output = NULL; if (cmd_info->pre_fn != NULL) { exit_code = cmd_info->pre_fn(out, &call_options, input, error); } if ((exit_code != CRM_EX_OK) || (cmd_info->cib_request == NULL)) { goto done; } if (options.section_type == cibadmin_section_xpath) { // Enable getting section by XPath cib__set_call_options(call_options, crm_system_name, cib_xpath); } else if ((options.section_type == cibadmin_section_scope) && !scope_is_valid(options.cib_section)) { // @COMPAT: Consider requiring --force to proceed out->err(out, "Invalid value '%s' for '--scope'. Operation will apply to the " "entire CIB", options.cib_section); } rc = cib__create_signon(&cib_conn); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(error, PCMK__EXITC_ERROR, exit_code, "Could not connect to the CIB API: %s", pcmk_rc_str(rc)); goto done; } cib_conn->call_timeout = options.timeout_sec; if (cib_conn->call_timeout < 1) { out->err(out, "Timeout must be positive, defaulting to %d", DEFAULT_TIMEOUT); cib_conn->call_timeout = DEFAULT_TIMEOUT; } rc = cib_internal_op(cib_conn, cmd_info->cib_request, options.dest_node, options.cib_section, input, &output, call_options, options.cib_user); rc = pcmk_legacy2rc(rc); if (cmd_info->post_fn != NULL) { exit_code = cmd_info->post_fn(out, cib_conn, call_options, output, rc, error); } else { exit_code = cibadmin_post_default(out, cib_conn, call_options, output, rc, error); } if (output != NULL) { cibadmin_output_xml(out, output, call_options, acl_user, &exit_code, error); } done: pcmk__xml_free(output); rc = cib__clean_up_connection(&cib_conn); if (exit_code == CRM_EX_OK) { exit_code = pcmk_rc2exitc(rc); } return exit_code; } static const cibadmin_cmd_info_t cibadmin_command_info[] = { [cibadmin_cmd_bump] = { PCMK__CIB_REQUEST_BUMP, NULL, NULL, cibadmin_cf_none, }, [cibadmin_cmd_create] = { PCMK__CIB_REQUEST_CREATE, NULL, NULL, cibadmin_cf_requires_input, }, [cibadmin_cmd_delete] = { PCMK__CIB_REQUEST_DELETE, cibadmin_pre_delete_all, NULL, cibadmin_cf_requires_input|cibadmin_cf_xpath_input, }, [cibadmin_cmd_delete_all] = { PCMK__CIB_REQUEST_DELETE, NULL, NULL, cibadmin_cf_requires_input|cibadmin_cf_unsafe|cibadmin_cf_xpath_input, }, [cibadmin_cmd_empty] = { NULL, cibadmin_pre_empty, NULL, cibadmin_cf_none, }, [cibadmin_cmd_erase] = { PCMK__CIB_REQUEST_ERASE, NULL, NULL, cibadmin_cf_unsafe, }, [cibadmin_cmd_md5_sum] = { NULL, cibadmin_pre_md5_sum, NULL, cibadmin_cf_requires_input, }, [cibadmin_cmd_md5_sum_versioned] = { NULL, cibadmin_pre_md5_sum_versioned, NULL, cibadmin_cf_requires_input, }, [cibadmin_cmd_modify] = { PCMK__CIB_REQUEST_MODIFY, cibadmin_pre_modify, NULL, cibadmin_cf_requires_input, }, [cibadmin_cmd_patch] = { PCMK__CIB_REQUEST_APPLY_PATCH, NULL, NULL, cibadmin_cf_requires_input, }, [cibadmin_cmd_query] = { PCMK__CIB_REQUEST_QUERY, cibadmin_pre_query, NULL, cibadmin_cf_none, }, [cibadmin_cmd_replace] = { PCMK__CIB_REQUEST_REPLACE, cibadmin_pre_replace, NULL, cibadmin_cf_requires_input, }, /* @TODO Ideally, --upgrade wouldn't be considered unsafe if the CIB already * uses the latest schema. */ [cibadmin_cmd_upgrade] = { PCMK__CIB_REQUEST_UPGRADE, NULL, cibadmin_post_upgrade, cibadmin_cf_unsafe, }, }; static gboolean command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_any_of(option_name, "-u", "--upgrade", NULL)) { options.cmd = cibadmin_cmd_upgrade; } else if (pcmk__str_any_of(option_name, "-Q", "--query", NULL)) { options.cmd = cibadmin_cmd_query; } else if (pcmk__str_any_of(option_name, "-E", "--erase", NULL)) { options.cmd = cibadmin_cmd_erase; } else if (pcmk__str_any_of(option_name, "-B", "--bump", NULL)) { options.cmd = cibadmin_cmd_bump; } else if (pcmk__str_any_of(option_name, "-C", "--create", NULL)) { options.cmd = cibadmin_cmd_create; } else if (pcmk__str_any_of(option_name, "-M", "--modify", NULL)) { options.cmd = cibadmin_cmd_modify; } else if (pcmk__str_any_of(option_name, "-P", "--patch", NULL)) { options.cmd = cibadmin_cmd_patch; } else if (pcmk__str_any_of(option_name, "-R", "--replace", NULL)) { options.cmd = cibadmin_cmd_replace; } else if (pcmk__str_any_of(option_name, "-D", "--delete", NULL)) { options.cmd = cibadmin_cmd_delete; } else if (pcmk__str_any_of(option_name, "-d", "--delete-all", NULL)) { options.cmd = cibadmin_cmd_delete_all; } else if (pcmk__str_any_of(option_name, "-a", "--empty", NULL)) { options.cmd = cibadmin_cmd_empty; pcmk__str_update(&options.validate_with, optarg); } else if (pcmk__str_any_of(option_name, "-5", "--md5-sum", NULL)) { options.cmd = cibadmin_cmd_md5_sum; } else if (pcmk__str_any_of(option_name, "-6", "--md5-sum-versioned", NULL)) { options.cmd = cibadmin_cmd_md5_sum_versioned; } else { // Should be impossible return FALSE; } return TRUE; } static gboolean show_access_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_eq(optarg, "auto", pcmk__str_null_matches)) { options.acl_render_mode = pcmk__acl_render_default; } else if (g_strcmp0(optarg, "namespace") == 0) { options.acl_render_mode = pcmk__acl_render_namespace; } else if (g_strcmp0(optarg, "text") == 0) { options.acl_render_mode = pcmk__acl_render_text; } else if (g_strcmp0(optarg, "color") == 0) { options.acl_render_mode = pcmk__acl_render_color; } else { g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_USAGE, "Invalid value '%s' for option '%s'", optarg, option_name); return FALSE; } return TRUE; } static gboolean section_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_any_of(option_name, "-o", "--scope", NULL)) { options.section_type = cibadmin_section_scope; } else if (pcmk__str_any_of(option_name, "-A", "--xpath", NULL)) { options.section_type = cibadmin_section_xpath; } else { // Should be impossible return FALSE; } pcmk__str_update(&options.cib_section, optarg); return TRUE; } static GOptionEntry command_entries[] = { { "upgrade", 'u', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Upgrade the configuration to the latest syntax", NULL }, { "query", 'Q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Query the contents of the CIB", NULL }, { "erase", 'E', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Erase the contents of the whole CIB", NULL }, { "bump", 'B', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Increase the CIB's epoch value by 1", NULL }, { "create", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Create an object in the CIB (will fail if object already exists)", NULL }, { "modify", 'M', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Find object somewhere in CIB's XML tree and update it (fails if object " "does not exist unless -c is also specified)", NULL }, { "patch", 'P', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Supply an update in the form of an XML diff (see crm_diff(8))", NULL }, { "replace", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Recursively replace an object in the CIB", NULL }, { "delete", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Delete first object matching supplied criteria (for example, " "<" PCMK_XE_OP " " PCMK_XA_ID "=\"rsc1_op1\" " PCMK_XA_NAME "=\"monitor\"/>).\n" INDENT "The XML element name and all attributes must match in order for " "the element to be deleted.", NULL }, { "delete-all", 'd', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "When used with --xpath, remove all matching objects in the " "configuration instead of just the first one", NULL }, { "empty", 'a', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Output an empty CIB. Accepts an optional schema name argument to use as " "the " PCMK_XA_VALIDATE_WITH " value.\n" INDENT "If no schema is given, the latest will be used.", "[schema]" }, { "md5-sum", '5', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Calculate the on-disk CIB digest", NULL }, { "md5-sum-versioned", '6', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Calculate an on-the-wire versioned CIB digest", NULL }, { NULL } }; static GOptionEntry data_entries[] = { // @COMPAT These arguments should be last-one-wins { "xml-file", 'x', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &options.input_file, "Retrieve XML from the named file. Currently this takes precedence\n" INDENT "over --xml-text and --xml-pipe. In a future release, the last\n" INDENT "one specified will be used.", "FILE" }, { "xml-text", 'X', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.input_string, "Retrieve XML from the supplied string. Currently this takes precedence\n" INDENT "over --xml-pipe, but --xml-file overrides this. In a future\n" INDENT "release, the last one specified will be used.", "STRING" }, { "xml-pipe", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.input_stdin, "Retrieve XML from stdin. Currently --xml-file and --xml-text override\n" INDENT "this. In a future release, the last one specified will be used.", NULL }, { NULL } }; static GOptionEntry addl_entries[] = { { "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force, "Force the action to be performed", NULL }, { "timeout", 't', G_OPTION_FLAG_NONE, G_OPTION_ARG_INT, &options.timeout_sec, "Time (in seconds) to wait before declaring the operation failed", "value" }, { "user", 'U', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.cib_user, "Run the command with permissions of the named user (valid only for the " "root and " CRM_DAEMON_USER " accounts)", "value" }, { "scope", 'o', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, section_cb, "Limit scope of operation to specific section of CIB\n" INDENT "Valid values: " PCMK_XE_CONFIGURATION ", " PCMK_XE_NODES ", " PCMK_XE_RESOURCES ", " PCMK_XE_CONSTRAINTS ", " PCMK_XE_CRM_CONFIG ", " PCMK_XE_RSC_DEFAULTS ",\n" INDENT " " PCMK_XE_OP_DEFAULTS ", " PCMK_XE_ACLS ", " PCMK_XE_FENCING_TOPOLOGY ", " PCMK_XE_TAGS ", " PCMK_XE_ALERTS ", " PCMK_XE_STATUS "\n" INDENT "If both --scope/-o and --xpath/-a are specified, the last one to " "appear takes effect", "value" }, { "xpath", 'A', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, section_cb, "A valid XPath to use instead of --scope/-o\n" INDENT "If both --scope/-o and --xpath/-a are specified, the last one to " "appear takes effect", "value" }, { "show-access", 'S', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, show_access_cb, "Whether to use syntax highlighting for ACLs (with -Q/--query and " "-U/--user)\n" INDENT "Allowed values: 'color' (default for terminal), 'text' (plain text, " "default for non-terminal),\n" INDENT " 'namespace', or 'auto' (use default value)\n" INDENT "Default value: 'auto'", "[value]" }, { "score", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.score_update, "Treat new attribute values as atomic score updates where possible " "(with --modify/-M).\n" INDENT "This currently happens by default and cannot be disabled, but\n" INDENT "this default behavior is deprecated and will be removed in a\n" INDENT "future release. Set this flag if this behavior is desired.\n" INDENT "This option takes effect when updating XML attributes. For an\n" INDENT "attribute named \"name\", if the new value is \"name++\" or\n" INDENT "\"name+=X\" for some score X, the new value is set as follows:\n" INDENT "If attribute \"name\" is not already set to some value in\n" INDENT "the element being updated, the new value is set as a literal\n" INDENT "string.\n" INDENT "If the new value is \"name++\", then the attribute is set to \n" INDENT "its existing value (parsed as a score) plus 1.\n" INDENT "If the new value is \"name+=X\" for some score X, then the\n" INDENT "attribute is set to its existing value plus X, where the\n" INDENT "existing value and X are parsed and added as scores.\n" INDENT "Scores are integer values capped at INFINITY and -INFINITY.\n" INDENT "Refer to Pacemaker Explained for more details on scores,\n" INDENT "including how they are parsed and added.", NULL }, { "allow-create", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.allow_create, "(Advanced) Allow target of --modify/-M to be created if it does not " "exist", NULL }, { "no-children", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.no_children, "(Advanced) When querying an object, do not include its children in the " "result", NULL }, // @COMPAT Deprecated since 3.0.0 { "local", 'l', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.local, "(deprecated)", NULL }, // @COMPAT Deprecated since 3.0.2 { "node", 'N', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.dest_node, "(deprecated)", "value" }, // @COMPAT Deprecated since 3.0.2 { "node-path", 'e', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.get_node_path, "(deprecated)", NULL }, // @COMPAT Deprecated since 3.0.1 { "sync-call", 's', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.sync_call, "(deprecated)", NULL }, { NULL } }; PCMK__OUTPUT_ARGS("cibadmin-md5-sum", "const char *") static int md5_sum_default(pcmk__output_t *out, va_list args) { const char *digest = va_arg(args, const char *); if (digest == NULL) { return pcmk_rc_no_output; } return out->info(out, "%s", digest); } PCMK__OUTPUT_ARGS("cibadmin-md5-sum", "const char *") static int md5_sum_xml(pcmk__output_t *out, va_list args) { const char *digest = va_arg(args, const char *); if (digest == NULL) { return pcmk_rc_no_output; } pcmk__output_create_xml_node(out, PCMK_XE_MD5_SUM, PCMK_XA_DIGEST, digest, NULL); return pcmk_rc_ok; } // @COMPAT Drop "cibadmin-node-path" and helper when dropping --node-path static int output_xml_id(xmlNode *xml, void *user_data) { pcmk__output_t *out = user_data; const char *id = pcmk__xe_id(xml); pcmk__assert(id != NULL); return out->info(out, "%s", id); } PCMK__OUTPUT_ARGS("cibadmin-node-path", "xmlNode *") static int node_path_default(pcmk__output_t *out, va_list args) { xmlNode *query_result = va_arg(args, xmlNode *); if (query_result == NULL) { return pcmk_rc_no_output; } return pcmk__xe_foreach_child(query_result, PCMK__XE_XPATH_QUERY_PATH, output_xml_id, out); } PCMK__OUTPUT_ARGS("cibadmin-node-path", "xmlNode *") static int node_path_xml(pcmk__output_t *out, va_list args) { xmlNode *query_result = va_arg(args, xmlNode *); if (query_result == NULL) { return pcmk_rc_no_output; } cibadmin_output_basic_xml(out, query_result); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("cibadmin-rendered-acls", "const char *") static int rendered_acls_default(pcmk__output_t *out, va_list args) { const char *rendered = va_arg(args, const char *); if (rendered == NULL) { return pcmk_rc_no_output; } return out->info(out, "%s", rendered); } PCMK__OUTPUT_ARGS("cibadmin-rendered-acls", "const char *") static int rendered_acls_xml(pcmk__output_t *out, va_list args) { /* We want to create a CData block in a PCMK_XE_OUTPUT element. At the time * of writing, that's exactly what this call to xml_output_xml() does. * Note, however, that the "rendered" string is not XML if the ACL render * mode is color or text. * * @TODO Create a pcmk__output_xml_create_cdata() or similar, and share it * between xml_output_xml() and this function? */ const char *rendered = va_arg(args, const char *); if (rendered == NULL) { return pcmk_rc_no_output; } out->output_xml(out, PCMK_XE_OUTPUT, rendered); return pcmk_rc_ok; } static const pcmk__supported_format_t formats[] = { PCMK__SUPPORTED_FORMAT_NONE, PCMK__SUPPORTED_FORMAT_TEXT, PCMK__SUPPORTED_FORMAT_XML, { NULL, NULL, NULL } }; static const pcmk__message_entry_t fmt_functions[] = { { "cibadmin-md5-sum", "default", md5_sum_default }, { "cibadmin-md5-sum", "xml", md5_sum_xml }, { "cibadmin-node-path", "default", node_path_default }, { "cibadmin-node-path", "xml", node_path_xml }, { "cibadmin-rendered-acls", "default", rendered_acls_default }, { "cibadmin-rendered-acls", "xml", rendered_acls_xml }, { NULL, NULL, NULL } }; static GOptionContext * build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) { const char *desc = NULL; GOptionContext *context = NULL; desc = "Examples:\n\n" "Query the configuration:\n\n" "\t# cibadmin --query\n\n" "or just:\n\n" "\t# cibadmin\n\n" "Query just the cluster options configuration:\n\n" "\t# cibadmin --query --scope " PCMK_XE_CRM_CONFIG "\n\n" "Query all '" PCMK_META_TARGET_ROLE "' settings:\n\n" "\t# cibadmin --query --xpath " "\"//" PCMK_XE_NVPAIR "[@" PCMK_XA_NAME "='" PCMK_META_TARGET_ROLE"']\"\n\n" "Remove all '" PCMK_META_IS_MANAGED "' settings:\n\n" "\t# cibadmin --delete-all --xpath " "\"//" PCMK_XE_NVPAIR "[@" PCMK_XA_NAME "='" PCMK_META_IS_MANAGED "']\"\n\n" "Remove the resource named 'old':\n\n" "\t# cibadmin --delete --xml-text " "'<" PCMK_XE_PRIMITIVE " " PCMK_XA_ID "=\"old\"/>'\n\n" "Remove all resources from the configuration:\n\n" "\t# cibadmin --replace --scope " PCMK_XE_RESOURCES " --xml-text '<" PCMK_XE_RESOURCES "/>'\n\n" "Replace complete configuration with contents of " "$HOME/pacemaker.xml:\n\n" "\t# cibadmin --replace --xml-file $HOME/pacemaker.xml\n\n" "Replace " PCMK_XE_CONSTRAINTS " section of configuration with " "contents of $HOME/constraints.xml:\n\n" "\t# cibadmin --replace --scope " PCMK_XE_CONSTRAINTS " --xml-file $HOME/constraints.xml\n\n" "Increase configuration version to prevent old configurations from " "being loaded accidentally:\n\n" "\t# cibadmin --modify --score --xml-text " "'<" PCMK_XE_CIB " " PCMK_XA_ADMIN_EPOCH "=\"" PCMK_XA_ADMIN_EPOCH "++\"/>'\n\n" "Edit the configuration with your favorite $EDITOR:\n\n" "\t# cibadmin --query > $HOME/local.xml\n\n" "\t# $EDITOR $HOME/local.xml\n\n" "\t# cibadmin --replace --xml-file $HOME/local.xml\n\n" "Assuming terminal, render configuration in color (green for " "writable, blue for readable, red for\n" "denied) to visualize permissions for user tony:\n\n" "\t# cibadmin --show-access=color --query --user tony | less -r\n\n" "SEE ALSO:\n" " crm(8), pcs(8), crm_shadow(8), crm_diff(8)\n"; context = pcmk__build_arg_context(args, "text (default), xml", group, "[]"); g_option_context_set_description(context, desc); pcmk__add_arg_group(context, "commands", "Commands:", "Show command help", command_entries); pcmk__add_arg_group(context, "data", "Data:", "Show data help", data_entries); pcmk__add_arg_group(context, "additional", "Additional Options:", "Show additional options", addl_entries); return context; } /*! * \internal * \brief Read input XML as specified on the command line * * Precedence is as follows: * 1. Input file * 2. Input string * 3. stdin * * If multiple input sources are given, only the last occurrence of the one with * the highest precedence is tried. * * If no input source is specified, this function does nothing. * * \param[out] input Where to store parsed input * \param[out] source Where to store string describing input source * * \return Standard Pacemaker return code */ static int read_input(xmlNode **input, const char **source) { if (options.input_file != NULL) { *source = options.input_file; *input = pcmk__xml_read(options.input_file); } else if (options.input_string != NULL) { *source = "input string"; *input = pcmk__xml_parse(options.input_string); } else if (options.input_stdin) { *source = "stdin"; *input = pcmk__xml_read(NULL); } else { *source = NULL; *input = NULL; return EINVAL; } if (*input == NULL) { return pcmk_rc_bad_input; } return pcmk_rc_ok; } int main(int argc, char **argv) { int rc = pcmk_rc_ok; crm_exit_t exit_code = CRM_EX_OK; const cibadmin_cmd_info_t *cmd_info = NULL; int call_options = cib_sync_call; xmlNode *input = NULL; gchar *acl_cred = NULL; pcmk__output_t *out = NULL; GError *error = NULL; GOptionGroup *output_group = NULL; pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY); gchar **processed_args = pcmk__cmdline_preproc(argv, "ANSUXhotx"); GOptionContext *context = build_arg_context(args, &output_group); pcmk__register_formats(output_group, formats); if (!g_option_context_parse_strv(context, &processed_args, &error)) { exit_code = CRM_EX_USAGE; goto done; } /* At LOG_ERR, stderr for CIB calls is rather verbose. Several lines like * * (func@file:line) error: CIB failures * * In cibadmin we explicitly output the XML portion without the prefixes. So * we default to LOG_CRIT. */ pcmk__cli_init_logging("cibadmin", 0); set_crm_log_level(LOG_CRIT); if (args->verbosity > 0) { cib__set_call_options(call_options, crm_system_name, cib_verbose); for (int i = 0; i < args->verbosity; i++) { crm_bump_log_level(argc, argv); } } rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv); if (rc != pcmk_rc_ok) { exit_code = CRM_EX_ERROR; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Error creating output format %s: %s", args->output_ty, pcmk_rc_str(rc)); goto done; } if (g_strv_length(processed_args) > 1) { gchar *extra = g_strjoinv(" ", processed_args + 1); gchar *help = g_option_context_get_help(context, TRUE, NULL); exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "non-option ARGV-elements: %s\n\n%s", extra, help); g_free(extra); g_free(help); goto done; } if (args->version) { out->version(out); goto done; } pcmk__register_messages(out, fmt_functions); // Ensure command is in valid range if ((options.cmd >= 0) && (options.cmd <= cibadmin_cmd_max)) { cmd_info = &cibadmin_command_info[options.cmd]; } if (cmd_info == NULL) { exit_code = CRM_EX_SOFTWARE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Bug: Unimplemented command: %d", (int) options.cmd); goto done; } if (pcmk_is_set(cmd_info->flags, cibadmin_cf_unsafe) && !options.force) { exit_code = CRM_EX_UNSAFE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "The supplied command is considered dangerous. To prevent " "accidental destruction of the cluster, the --force flag " "is required in order to proceed."); goto done; } /* Query is the only command that produces output suitable for ACL * rendering. Ignore --show-access for other commands. */ if (options.acl_render_mode != pcmk__acl_render_none) { if (options.cmd == cibadmin_cmd_query) { char *username = NULL; if (options.cib_user == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "-U/--user is required with -S/--show-access"); goto done; } - // @COMPAT Fail if pcmk_acl_required(username) username = pcmk__uid2username(geteuid()); + if (username == NULL) { + exit_code = CRM_EX_NOSUCH; + g_set_error(&error, PCMK__EXITC_ERROR, exit_code, + "Failed to get username from password database for " + "effective user ID %lld", (long long) geteuid()); + goto done; + } + + // @COMPAT Fail if pcmk_acl_required(username) if (pcmk_acl_required(username)) { out->err(out, "Warning: cibadmin is being run as user %s, which is " "subject to ACLs. As a result, ACLs for user %s may " "be incorrect or incomplete in the output. In a " "future release, running as a privileged user (root " "or " CRM_DAEMON_USER ") will be required for " "-S/--show-access.", username, options.cib_user); } free(username); /* Note: acl_cred takes ownership of options.cib_user here. * options.cib_user is set to NULL so that the CIB is obtained as * the user running the cibadmin command. The CIB must be obtained * as a user with full permissions in order to show the CIB * correctly annotated for the options.cib_user's permissions. */ acl_cred = options.cib_user; options.cib_user = NULL; } else { options.acl_render_mode = pcmk__acl_render_none; } } if (pcmk_is_set(cmd_info->flags, cibadmin_cf_requires_input)) { bool accepts_xpath = pcmk_is_set(cmd_info->flags, cibadmin_cf_xpath_input); /* If true, use options.cib_section (an XPath expression) instead of * input XML */ bool as_xpath = accepts_xpath && (options.section_type == cibadmin_section_xpath); if (!as_xpath) { const char *source = NULL; rc = read_input(&input, &source); if (rc == EINVAL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "The supplied command requires %sinput via " "--xml-file, --xml-text, or --xml-pipe", (accepts_xpath? "either --xpath or " : "")); goto done; } if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Couldn't parse input from %s", pcmk__s(source, "(BUG: null source)")); goto done; } } } exit_code = cibadmin_handle_command(out, cmd_info, call_options, acl_cred, input, &error); done: g_strfreev(processed_args); pcmk__free_arg_context(context); g_free(options.cib_user); g_free(options.dest_node); g_free(options.input_file); g_free(options.input_string); free(options.cib_section); free(options.validate_with); g_free(acl_cred); pcmk__xml_free(input); pcmk__output_and_clear_error(&error, out); if (out != NULL) { out->finish(out, exit_code, true, NULL); pcmk__output_free(out); } pcmk__unregister_formats(); crm_exit(exit_code); }