diff --git a/cts/cli/regression.acls.exp b/cts/cli/regression.acls.exp index 8e6ff5c4ab..22c2ce9f94 100644 --- a/cts/cli/regression.acls.exp +++ b/cts/cli/regression.acls.exp @@ -1,2344 +1,2348 @@ Created new pacemaker configuration Setting up shadow instance A new shadow instance was created. To begin using it paste the following into your shell: CIB_shadow=cts-cli ; export CIB_shadow =#=#=#= Begin test: Configure some ACLs =#=#=#= =#=#=#= Current cib after: Configure some ACLs =#=#=#= =#=#=#= End test: Configure some ACLs - OK (0) =#=#=#= * Passed: cibadmin - Configure some ACLs =#=#=#= Begin test: Enable ACLs =#=#=#= =#=#=#= Current cib after: Enable ACLs =#=#=#= =#=#=#= End test: Enable ACLs - OK (0) =#=#=#= * Passed: crm_attribute - Enable ACLs =#=#=#= Begin test: Set cluster option =#=#=#= =#=#=#= Current cib after: Set cluster option =#=#=#= =#=#=#= End test: Set cluster option - OK (0) =#=#=#= * Passed: crm_attribute - Set cluster option =#=#=#= Begin test: New ACL =#=#=#= =#=#=#= Current cib after: New ACL =#=#=#= =#=#=#= End test: New ACL - OK (0) =#=#=#= * Passed: cibadmin - New ACL =#=#=#= Begin test: Another ACL =#=#=#= =#=#=#= Current cib after: Another ACL =#=#=#= =#=#=#= End test: Another ACL - OK (0) =#=#=#= * Passed: cibadmin - Another ACL =#=#=#= Begin test: Updated ACL =#=#=#= =#=#=#= Current cib after: Updated ACL =#=#=#= =#=#=#= End test: Updated ACL - OK (0) =#=#=#= * Passed: cibadmin - Updated ACL =#=#=#= Begin test: unknownguy: Query configuration =#=#=#= Call failed: Permission denied =#=#=#= End test: unknownguy: Query configuration - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - unknownguy: Query configuration =#=#=#= Begin test: unknownguy: Set enable-acl =#=#=#= Error performing operation: Permission denied =#=#=#= End test: unknownguy: Set enable-acl - Insufficient privileges (4) =#=#=#= * Passed: crm_attribute - unknownguy: Set enable-acl =#=#=#= Begin test: unknownguy: Set stonith-enabled =#=#=#= Error performing operation: Permission denied =#=#=#= End test: unknownguy: Set stonith-enabled - Insufficient privileges (4) =#=#=#= * Passed: crm_attribute - unknownguy: Set stonith-enabled =#=#=#= Begin test: unknownguy: Create a resource =#=#=#= -pcmk__check_acl trace: Ordinary user unknownguy cannot access the CIB without any defined ACLs -pcmk__check_acl trace: Ordinary user unknownguy cannot access the CIB without any defined ACLs -pcmk__check_acl trace: Ordinary user unknownguy cannot access the CIB without any defined ACLs -pcmk__check_acl trace: Ordinary user unknownguy cannot access the CIB without any defined ACLs +pcmk__check_acl trace: User 'unknownguy' without ACLs denied read/write access to /cib/configuration/resources/primitive[@id] +pcmk__check_acl trace: User 'unknownguy' without ACLs denied read/write access to /cib/configuration/resources/primitive[@class] +pcmk__check_acl trace: User 'unknownguy' without ACLs denied read/write access to /cib/configuration/resources/primitive[@provider] +pcmk__check_acl trace: User 'unknownguy' without ACLs denied read/write access to /cib/configuration/resources/primitive[@type] +pcmk__apply_creation_acl trace: Creation of scaffolding with id="" is implicitly allowed Call failed: Permission denied =#=#=#= End test: unknownguy: Create a resource - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - unknownguy: Create a resource =#=#=#= Begin test: l33t-haxor: Query configuration =#=#=#= Call failed: Permission denied =#=#=#= End test: l33t-haxor: Query configuration - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - l33t-haxor: Query configuration =#=#=#= Begin test: l33t-haxor: Set enable-acl =#=#=#= Error performing operation: Permission denied =#=#=#= End test: l33t-haxor: Set enable-acl - Insufficient privileges (4) =#=#=#= * Passed: crm_attribute - l33t-haxor: Set enable-acl =#=#=#= Begin test: l33t-haxor: Set stonith-enabled =#=#=#= Error performing operation: Permission denied =#=#=#= End test: l33t-haxor: Set stonith-enabled - Insufficient privileges (4) =#=#=#= * Passed: crm_attribute - l33t-haxor: Set stonith-enabled =#=#=#= Begin test: l33t-haxor: Create a resource =#=#=#= -pcmk__check_acl trace: 400 access denied to /cib/configuration/resources/primitive[@id='dummy']: parent -pcmk__post_process_acl trace: Cannot add new node primitive at /cib/configuration/resources/primitive[@id='dummy'] +pcmk__check_acl trace: Parent ACL denies user 'l33t-haxor' read/write access to /cib/configuration/resources/primitive[@id='dummy'] +pcmk__apply_creation_acl trace: ACLs disallow creation of with id="dummy" Call failed: Permission denied =#=#=#= End test: l33t-haxor: Create a resource - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - l33t-haxor: Create a resource =#=#=#= Begin test: niceguy: Query configuration =#=#=#= =#=#=#= End test: niceguy: Query configuration - OK (0) =#=#=#= * Passed: cibadmin - niceguy: Query configuration =#=#=#= Begin test: niceguy: Set enable-acl =#=#=#= -pcmk__check_acl trace: 400 access denied to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl'][@value]: default +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl'][@value] Error performing operation: Permission denied Error setting enable-acl=false (section=crm_config, set=): Permission denied =#=#=#= End test: niceguy: Set enable-acl - Insufficient privileges (4) =#=#=#= * Passed: crm_attribute - niceguy: Set enable-acl =#=#=#= Begin test: niceguy: Set stonith-enabled =#=#=#= -pcmk__post_process_acl trace: Creation of nvpair=cib-bootstrap-options-stonith-enabled is allowed +pcmk__apply_creation_acl trace: ACLs allow creation of with id="cib-bootstrap-options-stonith-enabled" =#=#=#= Current cib after: niceguy: Set stonith-enabled =#=#=#= =#=#=#= End test: niceguy: Set stonith-enabled - OK (0) =#=#=#= * Passed: crm_attribute - niceguy: Set stonith-enabled =#=#=#= Begin test: niceguy: Create a resource =#=#=#= -pcmk__check_acl trace: 400 access denied to /cib/configuration/resources/primitive[@id='dummy']: default -pcmk__post_process_acl trace: Cannot add new node primitive at /cib/configuration/resources/primitive[@id='dummy'] +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/resources/primitive[@id='dummy'] +pcmk__apply_creation_acl trace: ACLs disallow creation of with id="dummy" Call failed: Permission denied =#=#=#= End test: niceguy: Create a resource - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - niceguy: Create a resource =#=#=#= Begin test: root: Query configuration =#=#=#= =#=#=#= End test: root: Query configuration - OK (0) =#=#=#= * Passed: cibadmin - root: Query configuration =#=#=#= Begin test: root: Set stonith-enabled =#=#=#= =#=#=#= Current cib after: root: Set stonith-enabled =#=#=#= =#=#=#= End test: root: Set stonith-enabled - OK (0) =#=#=#= * Passed: crm_attribute - root: Set stonith-enabled =#=#=#= Begin test: root: Create a resource =#=#=#= =#=#=#= Current cib after: root: Create a resource =#=#=#= =#=#=#= End test: root: Create a resource - OK (0) =#=#=#= * Passed: cibadmin - root: Create a resource =#=#=#= Begin test: l33t-haxor: Create a resource meta attribute =#=#=#= Error performing operation: Permission denied =#=#=#= End test: l33t-haxor: Create a resource meta attribute - Insufficient privileges (4) =#=#=#= * Passed: crm_resource - l33t-haxor: Create a resource meta attribute =#=#=#= Begin test: l33t-haxor: Query a resource meta attribute =#=#=#= Error performing operation: Permission denied =#=#=#= End test: l33t-haxor: Query a resource meta attribute - Insufficient privileges (4) =#=#=#= * Passed: crm_resource - l33t-haxor: Query a resource meta attribute =#=#=#= Begin test: l33t-haxor: Remove a resource meta attribute =#=#=#= Error performing operation: Permission denied =#=#=#= End test: l33t-haxor: Remove a resource meta attribute - Insufficient privileges (4) =#=#=#= * Passed: crm_resource - l33t-haxor: Remove a resource meta attribute =#=#=#= Begin test: niceguy: Create a resource meta attribute =#=#=#= unpack_resources error: Resource start-up disabled since no STONITH resources have been defined unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity -pcmk__post_process_acl trace: Creation of nvpair=dummy-meta_attributes-target-role is allowed +pcmk__apply_creation_acl trace: Creation of scaffolding with id="dummy-meta_attributes" is implicitly allowed +pcmk__apply_creation_acl trace: ACLs allow creation of with id="dummy-meta_attributes-target-role" Set 'dummy' option: id=dummy-meta_attributes-target-role set=dummy-meta_attributes name=target-role value=Stopped =#=#=#= Current cib after: niceguy: Create a resource meta attribute =#=#=#= =#=#=#= End test: niceguy: Create a resource meta attribute - OK (0) =#=#=#= * Passed: crm_resource - niceguy: Create a resource meta attribute =#=#=#= Begin test: niceguy: Query a resource meta attribute =#=#=#= unpack_resources error: Resource start-up disabled since no STONITH resources have been defined unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity Stopped =#=#=#= Current cib after: niceguy: Query a resource meta attribute =#=#=#= =#=#=#= End test: niceguy: Query a resource meta attribute - OK (0) =#=#=#= * Passed: crm_resource - niceguy: Query a resource meta attribute =#=#=#= Begin test: niceguy: Remove a resource meta attribute =#=#=#= unpack_resources error: Resource start-up disabled since no STONITH resources have been defined unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity Deleted 'dummy' option: id=dummy-meta_attributes-target-role name=target-role =#=#=#= Current cib after: niceguy: Remove a resource meta attribute =#=#=#= =#=#=#= End test: niceguy: Remove a resource meta attribute - OK (0) =#=#=#= * Passed: crm_resource - niceguy: Remove a resource meta attribute =#=#=#= Begin test: niceguy: Create a resource meta attribute =#=#=#= unpack_resources error: Resource start-up disabled since no STONITH resources have been defined unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity -pcmk__post_process_acl trace: Creation of nvpair=dummy-meta_attributes-target-role is allowed +pcmk__apply_creation_acl trace: ACLs allow creation of with id="dummy-meta_attributes-target-role" Set 'dummy' option: id=dummy-meta_attributes-target-role set=dummy-meta_attributes name=target-role value=Started =#=#=#= Current cib after: niceguy: Create a resource meta attribute =#=#=#= =#=#=#= End test: niceguy: Create a resource meta attribute - OK (0) =#=#=#= * Passed: crm_resource - niceguy: Create a resource meta attribute =#=#=#= Begin test: badidea: Query configuration - implied deny =#=#=#= =#=#=#= End test: badidea: Query configuration - implied deny - OK (0) =#=#=#= * Passed: cibadmin - badidea: Query configuration - implied deny =#=#=#= Begin test: betteridea: Query configuration - explicit deny =#=#=#= =#=#=#= End test: betteridea: Query configuration - explicit deny - OK (0) =#=#=#= * Passed: cibadmin - betteridea: Query configuration - explicit deny =#=#=#= Begin test: niceguy: Replace - remove acls =#=#=#= -pcmk__check_acl trace: 400 access denied to /cib[@epoch]: default -pcmk__check_acl trace: 400 access denied to /cib/configuration/acls: default +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch] +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/acls Call failed: Permission denied =#=#=#= End test: niceguy: Replace - remove acls - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - niceguy: Replace - remove acls =#=#=#= Begin test: niceguy: Replace - create resource =#=#=#= -pcmk__check_acl trace: 400 access denied to /cib[@epoch]: default -pcmk__check_acl trace: 400 access denied to /cib/configuration/resources/primitive[@id='dummy2']: default -pcmk__post_process_acl trace: Cannot add new node primitive at /cib/configuration/resources/primitive[@id='dummy2'] +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch] +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/resources/primitive[@id='dummy2'] +pcmk__apply_creation_acl trace: ACLs disallow creation of with id="dummy2" Call failed: Permission denied =#=#=#= End test: niceguy: Replace - create resource - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - niceguy: Replace - create resource =#=#=#= Begin test: niceguy: Replace - modify attribute (deny) =#=#=#= -pcmk__check_acl trace: 400 access denied to /cib[@epoch]: default -pcmk__check_acl trace: 400 access denied to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl'][@value]: default +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch] +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl'][@value] Call failed: Permission denied =#=#=#= End test: niceguy: Replace - modify attribute (deny) - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - niceguy: Replace - modify attribute (deny) =#=#=#= Begin test: niceguy: Replace - delete attribute (deny) =#=#=#= -pcmk__check_acl trace: 400 access denied to /cib[@epoch]: default -pcmk__check_acl trace: 400 access denied to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl']: default +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch] +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl'] Call failed: Permission denied =#=#=#= End test: niceguy: Replace - delete attribute (deny) - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - niceguy: Replace - delete attribute (deny) =#=#=#= Begin test: niceguy: Replace - create attribute (deny) =#=#=#= -pcmk__check_acl trace: 400 access denied to /cib[@epoch]: default -pcmk__check_acl trace: 400 access denied to /cib/configuration/resources/primitive[@id='dummy'][@description]: default +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch] +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/resources/primitive[@id='dummy'][@description] Call failed: Permission denied =#=#=#= End test: niceguy: Replace - create attribute (deny) - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - niceguy: Replace - create attribute (deny) =#=#=#= Begin test: bob: Replace - create attribute (allow) =#=#=#= =#=#=#= End test: bob: Replace - create attribute (allow) - OK (0) =#=#=#= * Passed: cibadmin - bob: Replace - create attribute (allow) =#=#=#= Begin test: bob: Replace - modify attribute (allow) =#=#=#= =#=#=#= End test: bob: Replace - modify attribute (allow) - OK (0) =#=#=#= * Passed: cibadmin - bob: Replace - modify attribute (allow) =#=#=#= Begin test: bob: Replace - delete attribute (allow) =#=#=#= =#=#=#= End test: bob: Replace - delete attribute (allow) - OK (0) =#=#=#= * Passed: cibadmin - bob: Replace - delete attribute (allow) !#!#!#!#! Upgrading to latest CIB schema and re-testing !#!#!#!#! =#=#=#= Begin test: root: Upgrade to latest CIB schema =#=#=#= -pcmk__post_process_acl trace: Creation of acl_permission=observer-read-1 is allowed -pcmk__post_process_acl trace: Creation of acl_permission=observer-write-1 is allowed -pcmk__post_process_acl trace: Creation of acl_permission=observer-write-2 is allowed -pcmk__post_process_acl trace: Creation of acl_permission=admin-read-1 is allowed -pcmk__post_process_acl trace: Creation of acl_permission=admin-write-1 is allowed -pcmk__post_process_acl trace: Creation of acl_target=l33t-haxor is allowed -pcmk__post_process_acl trace: Creation of role=auto-l33t-haxor is allowed -pcmk__post_process_acl trace: Creation of acl_role=auto-l33t-haxor is allowed -pcmk__post_process_acl trace: Creation of acl_permission=crook-nothing is allowed -pcmk__post_process_acl trace: Creation of acl_target=niceguy is allowed -pcmk__post_process_acl trace: Creation of role=observer is allowed -pcmk__post_process_acl trace: Creation of acl_target=bob is allowed -pcmk__post_process_acl trace: Creation of role=admin is allowed -pcmk__post_process_acl trace: Creation of acl_target=badidea is allowed -pcmk__post_process_acl trace: Creation of role=auto-badidea is allowed -pcmk__post_process_acl trace: Creation of acl_role=auto-badidea is allowed -pcmk__post_process_acl trace: Creation of acl_permission=badidea-resources is allowed -pcmk__post_process_acl trace: Creation of acl_target=betteridea is allowed -pcmk__post_process_acl trace: Creation of role=auto-betteridea is allowed -pcmk__post_process_acl trace: Creation of acl_role=auto-betteridea is allowed -pcmk__post_process_acl trace: Creation of acl_permission=betteridea-nothing is allowed -pcmk__post_process_acl trace: Creation of acl_permission=betteridea-resources is allowed +pcmk__apply_creation_acl trace: ACLs allow creation of with id="observer-read-1" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="observer-write-1" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="observer-write-2" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="admin-read-1" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="admin-write-1" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="l33t-haxor" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="auto-l33t-haxor" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="auto-l33t-haxor" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="crook-nothing" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="niceguy" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="observer" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="bob" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="admin" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="badidea" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="auto-badidea" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="auto-badidea" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="badidea-resources" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="betteridea" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="auto-betteridea" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="auto-betteridea" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="betteridea-nothing" +pcmk__apply_creation_acl trace: ACLs allow creation of with id="betteridea-resources" =#=#=#= Current cib after: root: Upgrade to latest CIB schema =#=#=#= =#=#=#= End test: root: Upgrade to latest CIB schema - OK (0) =#=#=#= * Passed: cibadmin - root: Upgrade to latest CIB schema =#=#=#= Begin test: unknownguy: Query configuration =#=#=#= Call failed: Permission denied =#=#=#= End test: unknownguy: Query configuration - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - unknownguy: Query configuration =#=#=#= Begin test: unknownguy: Set enable-acl =#=#=#= Error performing operation: Permission denied =#=#=#= End test: unknownguy: Set enable-acl - Insufficient privileges (4) =#=#=#= * Passed: crm_attribute - unknownguy: Set enable-acl =#=#=#= Begin test: unknownguy: Set stonith-enabled =#=#=#= Error performing operation: Permission denied =#=#=#= End test: unknownguy: Set stonith-enabled - Insufficient privileges (4) =#=#=#= * Passed: crm_attribute - unknownguy: Set stonith-enabled =#=#=#= Begin test: unknownguy: Create a resource =#=#=#= -pcmk__check_acl trace: Ordinary user unknownguy cannot access the CIB without any defined ACLs -pcmk__check_acl trace: Ordinary user unknownguy cannot access the CIB without any defined ACLs -pcmk__check_acl trace: Ordinary user unknownguy cannot access the CIB without any defined ACLs -pcmk__check_acl trace: Ordinary user unknownguy cannot access the CIB without any defined ACLs +pcmk__check_acl trace: User 'unknownguy' without ACLs denied read/write access to /cib/configuration/resources/primitive[@id] +pcmk__check_acl trace: User 'unknownguy' without ACLs denied read/write access to /cib/configuration/resources/primitive[@class] +pcmk__check_acl trace: User 'unknownguy' without ACLs denied read/write access to /cib/configuration/resources/primitive[@provider] +pcmk__check_acl trace: User 'unknownguy' without ACLs denied read/write access to /cib/configuration/resources/primitive[@type] +pcmk__apply_creation_acl trace: Creation of scaffolding with id="" is implicitly allowed Call failed: Permission denied =#=#=#= End test: unknownguy: Create a resource - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - unknownguy: Create a resource =#=#=#= Begin test: l33t-haxor: Query configuration =#=#=#= Call failed: Permission denied =#=#=#= End test: l33t-haxor: Query configuration - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - l33t-haxor: Query configuration =#=#=#= Begin test: l33t-haxor: Set enable-acl =#=#=#= Error performing operation: Permission denied =#=#=#= End test: l33t-haxor: Set enable-acl - Insufficient privileges (4) =#=#=#= * Passed: crm_attribute - l33t-haxor: Set enable-acl =#=#=#= Begin test: l33t-haxor: Set stonith-enabled =#=#=#= Error performing operation: Permission denied =#=#=#= End test: l33t-haxor: Set stonith-enabled - Insufficient privileges (4) =#=#=#= * Passed: crm_attribute - l33t-haxor: Set stonith-enabled =#=#=#= Begin test: l33t-haxor: Create a resource =#=#=#= -pcmk__check_acl trace: 400 access denied to /cib/configuration/resources/primitive[@id='dummy']: parent -pcmk__post_process_acl trace: Cannot add new node primitive at /cib/configuration/resources/primitive[@id='dummy'] +pcmk__check_acl trace: Parent ACL denies user 'l33t-haxor' read/write access to /cib/configuration/resources/primitive[@id='dummy'] +pcmk__apply_creation_acl trace: ACLs disallow creation of with id="dummy" Call failed: Permission denied =#=#=#= End test: l33t-haxor: Create a resource - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - l33t-haxor: Create a resource =#=#=#= Begin test: niceguy: Query configuration =#=#=#= =#=#=#= End test: niceguy: Query configuration - OK (0) =#=#=#= * Passed: cibadmin - niceguy: Query configuration =#=#=#= Begin test: niceguy: Set enable-acl =#=#=#= -pcmk__check_acl trace: 400 access denied to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl'][@value]: default +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl'][@value] Error performing operation: Permission denied Error setting enable-acl=false (section=crm_config, set=): Permission denied =#=#=#= End test: niceguy: Set enable-acl - Insufficient privileges (4) =#=#=#= * Passed: crm_attribute - niceguy: Set enable-acl =#=#=#= Begin test: niceguy: Set stonith-enabled =#=#=#= =#=#=#= Current cib after: niceguy: Set stonith-enabled =#=#=#= =#=#=#= End test: niceguy: Set stonith-enabled - OK (0) =#=#=#= * Passed: crm_attribute - niceguy: Set stonith-enabled =#=#=#= Begin test: niceguy: Create a resource =#=#=#= -pcmk__check_acl trace: 400 access denied to /cib/configuration/resources/primitive[@id='dummy']: default -pcmk__post_process_acl trace: Cannot add new node primitive at /cib/configuration/resources/primitive[@id='dummy'] +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/resources/primitive[@id='dummy'] +pcmk__apply_creation_acl trace: ACLs disallow creation of with id="dummy" Call failed: Permission denied =#=#=#= End test: niceguy: Create a resource - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - niceguy: Create a resource =#=#=#= Begin test: root: Query configuration =#=#=#= =#=#=#= End test: root: Query configuration - OK (0) =#=#=#= * Passed: cibadmin - root: Query configuration =#=#=#= Begin test: root: Set stonith-enabled =#=#=#= =#=#=#= Current cib after: root: Set stonith-enabled =#=#=#= =#=#=#= End test: root: Set stonith-enabled - OK (0) =#=#=#= * Passed: crm_attribute - root: Set stonith-enabled =#=#=#= Begin test: root: Create a resource =#=#=#= =#=#=#= Current cib after: root: Create a resource =#=#=#= =#=#=#= End test: root: Create a resource - OK (0) =#=#=#= * Passed: cibadmin - root: Create a resource =#=#=#= Begin test: l33t-haxor: Create a resource meta attribute =#=#=#= Error performing operation: Permission denied =#=#=#= End test: l33t-haxor: Create a resource meta attribute - Insufficient privileges (4) =#=#=#= * Passed: crm_resource - l33t-haxor: Create a resource meta attribute =#=#=#= Begin test: l33t-haxor: Query a resource meta attribute =#=#=#= Error performing operation: Permission denied =#=#=#= End test: l33t-haxor: Query a resource meta attribute - Insufficient privileges (4) =#=#=#= * Passed: crm_resource - l33t-haxor: Query a resource meta attribute =#=#=#= Begin test: l33t-haxor: Remove a resource meta attribute =#=#=#= Error performing operation: Permission denied =#=#=#= End test: l33t-haxor: Remove a resource meta attribute - Insufficient privileges (4) =#=#=#= * Passed: crm_resource - l33t-haxor: Remove a resource meta attribute =#=#=#= Begin test: niceguy: Create a resource meta attribute =#=#=#= unpack_resources error: Resource start-up disabled since no STONITH resources have been defined unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity -pcmk__post_process_acl trace: Creation of nvpair=dummy-meta_attributes-target-role is allowed +pcmk__apply_creation_acl trace: Creation of scaffolding with id="dummy-meta_attributes" is implicitly allowed +pcmk__apply_creation_acl trace: ACLs allow creation of with id="dummy-meta_attributes-target-role" Set 'dummy' option: id=dummy-meta_attributes-target-role set=dummy-meta_attributes name=target-role value=Stopped =#=#=#= Current cib after: niceguy: Create a resource meta attribute =#=#=#= =#=#=#= End test: niceguy: Create a resource meta attribute - OK (0) =#=#=#= * Passed: crm_resource - niceguy: Create a resource meta attribute =#=#=#= Begin test: niceguy: Query a resource meta attribute =#=#=#= unpack_resources error: Resource start-up disabled since no STONITH resources have been defined unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity Stopped =#=#=#= Current cib after: niceguy: Query a resource meta attribute =#=#=#= =#=#=#= End test: niceguy: Query a resource meta attribute - OK (0) =#=#=#= * Passed: crm_resource - niceguy: Query a resource meta attribute =#=#=#= Begin test: niceguy: Remove a resource meta attribute =#=#=#= unpack_resources error: Resource start-up disabled since no STONITH resources have been defined unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity Deleted 'dummy' option: id=dummy-meta_attributes-target-role name=target-role =#=#=#= Current cib after: niceguy: Remove a resource meta attribute =#=#=#= =#=#=#= End test: niceguy: Remove a resource meta attribute - OK (0) =#=#=#= * Passed: crm_resource - niceguy: Remove a resource meta attribute =#=#=#= Begin test: niceguy: Create a resource meta attribute =#=#=#= unpack_resources error: Resource start-up disabled since no STONITH resources have been defined unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity -pcmk__post_process_acl trace: Creation of nvpair=dummy-meta_attributes-target-role is allowed +pcmk__apply_creation_acl trace: ACLs allow creation of with id="dummy-meta_attributes-target-role" Set 'dummy' option: id=dummy-meta_attributes-target-role set=dummy-meta_attributes name=target-role value=Started =#=#=#= Current cib after: niceguy: Create a resource meta attribute =#=#=#= =#=#=#= End test: niceguy: Create a resource meta attribute - OK (0) =#=#=#= * Passed: crm_resource - niceguy: Create a resource meta attribute =#=#=#= Begin test: badidea: Query configuration - implied deny =#=#=#= =#=#=#= End test: badidea: Query configuration - implied deny - OK (0) =#=#=#= * Passed: cibadmin - badidea: Query configuration - implied deny =#=#=#= Begin test: betteridea: Query configuration - explicit deny =#=#=#= =#=#=#= End test: betteridea: Query configuration - explicit deny - OK (0) =#=#=#= * Passed: cibadmin - betteridea: Query configuration - explicit deny =#=#=#= Begin test: niceguy: Replace - remove acls =#=#=#= -pcmk__check_acl trace: 400 access denied to /cib[@epoch]: default -pcmk__check_acl trace: 400 access denied to /cib/configuration/acls: default +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch] +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/acls Call failed: Permission denied =#=#=#= End test: niceguy: Replace - remove acls - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - niceguy: Replace - remove acls =#=#=#= Begin test: niceguy: Replace - create resource =#=#=#= -pcmk__check_acl trace: 400 access denied to /cib[@epoch]: default -pcmk__check_acl trace: 400 access denied to /cib/configuration/resources/primitive[@id='dummy2']: default -pcmk__post_process_acl trace: Cannot add new node primitive at /cib/configuration/resources/primitive[@id='dummy2'] +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch] +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/resources/primitive[@id='dummy2'] +pcmk__apply_creation_acl trace: ACLs disallow creation of with id="dummy2" Call failed: Permission denied =#=#=#= End test: niceguy: Replace - create resource - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - niceguy: Replace - create resource =#=#=#= Begin test: niceguy: Replace - modify attribute (deny) =#=#=#= -pcmk__check_acl trace: 400 access denied to /cib[@epoch]: default -pcmk__check_acl trace: 400 access denied to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl'][@value]: default +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch] +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl'][@value] Call failed: Permission denied =#=#=#= End test: niceguy: Replace - modify attribute (deny) - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - niceguy: Replace - modify attribute (deny) =#=#=#= Begin test: niceguy: Replace - delete attribute (deny) =#=#=#= -pcmk__check_acl trace: 400 access denied to /cib[@epoch]: default -pcmk__check_acl trace: 400 access denied to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl']: default +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch] +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options']/nvpair[@id='cib-bootstrap-options-enable-acl'] Call failed: Permission denied =#=#=#= End test: niceguy: Replace - delete attribute (deny) - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - niceguy: Replace - delete attribute (deny) =#=#=#= Begin test: niceguy: Replace - create attribute (deny) =#=#=#= -pcmk__check_acl trace: 400 access denied to /cib[@epoch]: default -pcmk__check_acl trace: 400 access denied to /cib/configuration/resources/primitive[@id='dummy'][@description]: default +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib[@epoch] +pcmk__check_acl trace: Default ACL denies user 'niceguy' read/write access to /cib/configuration/resources/primitive[@id='dummy'][@description] Call failed: Permission denied =#=#=#= End test: niceguy: Replace - create attribute (deny) - Insufficient privileges (4) =#=#=#= * Passed: cibadmin - niceguy: Replace - create attribute (deny) =#=#=#= Begin test: bob: Replace - create attribute (allow) =#=#=#= =#=#=#= End test: bob: Replace - create attribute (allow) - OK (0) =#=#=#= * Passed: cibadmin - bob: Replace - create attribute (allow) =#=#=#= Begin test: bob: Replace - modify attribute (allow) =#=#=#= =#=#=#= End test: bob: Replace - modify attribute (allow) - OK (0) =#=#=#= * Passed: cibadmin - bob: Replace - modify attribute (allow) =#=#=#= Begin test: bob: Replace - delete attribute (allow) =#=#=#= =#=#=#= End test: bob: Replace - delete attribute (allow) - OK (0) =#=#=#= * Passed: cibadmin - bob: Replace - delete attribute (allow) diff --git a/cts/cts-cli.in b/cts/cts-cli.in index be8dc21e58..9e8f928efb 100755 --- a/cts/cts-cli.in +++ b/cts/cts-cli.in @@ -1,1069 +1,1069 @@ #!@BASH_PATH@ # # Copyright 2008-2019 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. # # # Note on portable usage of sed: GNU/POSIX/*BSD sed have a limited subset of # compatible functionality. Do not use the -i option, alternation (\|), # \0, or character sequences such as \n or \s. # USAGE_TEXT="Usage: cts-cli [] Options: --help Display this text, then exit -V, --verbose Display any differences from expected output -t 'TEST [...]' Run only specified tests (default: 'dates tools acls validity upgrade rules') -p DIR Look for executables in DIR (may be specified multiple times) -v, --valgrind Run all commands under valgrind -s Save actual output as expected output" # If readlink supports -e (i.e. GNU), use it readlink -e / >/dev/null 2>/dev/null if [ $? -eq 0 ]; then test_home="$(dirname "$(readlink -e "$0")")" else test_home="$(dirname "$0")" fi : ${shadow=cts-cli} shadow_dir=$(mktemp -d ${TMPDIR:-/tmp}/cts-cli.shadow.XXXXXXXXXX) num_errors=0 num_passed=0 verbose=0 tests="dates tools acls validity upgrade rules" do_save=0 VALGRIND_CMD= VALGRIND_OPTS=" -q --gen-suppressions=all --show-reachable=no --leak-check=full --trace-children=no --time-stamp=yes --num-callers=20 --suppressions=$test_home/valgrind-pcmk.suppressions " # These constants must track crm_exit_t values CRM_EX_OK=0 CRM_EX_ERROR=1 CRM_EX_INSUFFICIENT_PRIV=4 CRM_EX_USAGE=64 CRM_EX_CONFIG=78 CRM_EX_OLD=103 CRM_EX_DIGEST=104 CRM_EX_NOSUCH=105 CRM_EX_UNSAFE=107 CRM_EX_EXISTS=108 CRM_EX_MULTIPLE=109 CRM_EX_EXPIRED=110 CRM_EX_NOT_YET_IN_EFFECT=111 function test_assert() { target=$1; shift cib=$1; shift app=`echo "$cmd" | sed 's/\ .*//'` printf "* Running: $app - $desc\n" 1>&2 printf "=#=#=#= Begin test: $desc =#=#=#=\n" eval $VALGRIND_CMD $cmd 2>&1 rc=$? if [ x$cib != x0 ]; then printf "=#=#=#= Current cib after: $desc =#=#=#=\n" CIB_user=root cibadmin -Q fi printf "=#=#=#= End test: $desc - $(crm_error --exit $rc) ($rc) =#=#=#=\n" if [ $rc -ne $target ]; then num_errors=$(( $num_errors + 1 )) printf "* Failed (rc=%.3d): %-14s - %s\n" $rc $app "$desc" printf "* Failed (rc=%.3d): %-14s - %s\n" $rc $app "$desc (`which $app`)" 1>&2 return exit $CRM_EX_ERROR else printf "* Passed: %-14s - %s\n" $app "$desc" num_passed=$(( $num_passed + 1 )) fi } function test_tools() { local TMPXML local TMPORIG TMPXML=$(mktemp ${TMPDIR:-/tmp}/cts-cli.tools.xml.XXXXXXXXXX) TMPORIG=$(mktemp ${TMPDIR:-/tmp}/cts-cli.tools.existing.xml.XXXXXXXXXX) export CIB_shadow_dir="${shadow_dir}" $VALGRIND_CMD crm_shadow --batch --force --create-empty $shadow 2>&1 export CIB_shadow=$shadow desc="Validate CIB" cmd="cibadmin -Q" test_assert $CRM_EX_OK desc="Configure something before erasing" cmd="crm_attribute -n cluster-delay -v 60s" test_assert $CRM_EX_OK desc="Require --force for CIB erasure" cmd="cibadmin -E" test_assert $CRM_EX_UNSAFE desc="Allow CIB erasure with --force" cmd="cibadmin -E --force" test_assert $CRM_EX_OK desc="Query CIB" cmd="cibadmin -Q > $TMPORIG" test_assert $CRM_EX_OK desc="Set cluster option" cmd="crm_attribute -n cluster-delay -v 60s" test_assert $CRM_EX_OK desc="Query new cluster option" cmd="cibadmin -Q -o crm_config | grep cib-bootstrap-options-cluster-delay" test_assert $CRM_EX_OK desc="Query cluster options" cmd="cibadmin -Q -o crm_config > $TMPXML" test_assert $CRM_EX_OK desc="Set no-quorum policy" cmd="crm_attribute -n no-quorum-policy -v ignore" test_assert $CRM_EX_OK desc="Delete nvpair" cmd="cibadmin -D -o crm_config --xml-text ''" test_assert $CRM_EX_OK desc="Create operation should fail" cmd="cibadmin -C -o crm_config --xml-file $TMPXML" test_assert $CRM_EX_EXISTS desc="Modify cluster options section" cmd="cibadmin -M -o crm_config --xml-file $TMPXML" test_assert $CRM_EX_OK desc="Query updated cluster option" cmd="cibadmin -Q -o crm_config | grep cib-bootstrap-options-cluster-delay" test_assert $CRM_EX_OK desc="Set duplicate cluster option" cmd="crm_attribute -n cluster-delay -v 40s -s duplicate" test_assert $CRM_EX_OK desc="Setting multiply defined cluster option should fail" cmd="crm_attribute -n cluster-delay -v 30s" test_assert $CRM_EX_MULTIPLE desc="Set cluster option with -s" cmd="crm_attribute -n cluster-delay -v 30s -s duplicate" test_assert $CRM_EX_OK desc="Delete cluster option with -i" cmd="crm_attribute -n cluster-delay -D -i cib-bootstrap-options-cluster-delay" test_assert $CRM_EX_OK desc="Create node1 and bring it online" cmd="crm_simulate --live-check --in-place --node-up=node1" test_assert $CRM_EX_OK desc="Create node attribute" cmd="crm_attribute -n ram -v 1024M -N node1 -t nodes" test_assert $CRM_EX_OK desc="Query new node attribute" cmd="cibadmin -Q -o nodes | grep node1-ram" test_assert $CRM_EX_OK desc="Set a transient (fail-count) node attribute" cmd="crm_attribute -n fail-count-foo -v 3 -N node1 -t status" test_assert $CRM_EX_OK desc="Query a fail count" cmd="crm_failcount --query -r foo -N node1" test_assert $CRM_EX_OK desc="Delete a transient (fail-count) node attribute" cmd="crm_attribute -n fail-count-foo -D -N node1 -t status" test_assert $CRM_EX_OK desc="Digest calculation" cmd="cibadmin -Q | cibadmin -5 -p 2>&1 > /dev/null" test_assert $CRM_EX_OK # This update will fail because it has version numbers desc="Replace operation should fail" cmd="cibadmin -R --xml-file $TMPORIG" test_assert $CRM_EX_OLD desc="Default standby value" cmd="crm_standby -N node1 -G" test_assert $CRM_EX_OK desc="Set standby status" cmd="crm_standby -N node1 -v true" test_assert $CRM_EX_OK desc="Query standby value" cmd="crm_standby -N node1 -G" test_assert $CRM_EX_OK desc="Delete standby value" cmd="crm_standby -N node1 -D" test_assert $CRM_EX_OK desc="Create a resource" cmd="cibadmin -C -o resources --xml-text ''" test_assert $CRM_EX_OK desc="Create a resource meta attribute" cmd="crm_resource -r dummy --meta -p is-managed -v false" test_assert $CRM_EX_OK desc="Query a resource meta attribute" cmd="crm_resource -r dummy --meta -g is-managed" test_assert $CRM_EX_OK desc="Remove a resource meta attribute" cmd="crm_resource -r dummy --meta -d is-managed" test_assert $CRM_EX_OK desc="Create a resource attribute" cmd="crm_resource -r dummy -p delay -v 10s" test_assert $CRM_EX_OK desc="List the configured resources" cmd="crm_resource -L" test_assert $CRM_EX_OK desc="Require a destination when migrating a resource that is stopped" cmd="crm_resource -r dummy -M" test_assert $CRM_EX_USAGE desc="Don't support migration to non-existent locations" cmd="crm_resource -r dummy -M -N i.dont.exist" test_assert $CRM_EX_NOSUCH desc="Create a fencing resource" cmd="cibadmin -C -o resources --xml-text ''" test_assert $CRM_EX_OK desc="Bring resources online" cmd="crm_simulate --live-check --in-place -S" test_assert $CRM_EX_OK desc="Try to move a resource to its existing location" cmd="crm_resource -r dummy --move --host node1" test_assert $CRM_EX_EXISTS desc="Move a resource from its existing location" cmd="crm_resource -r dummy --move" test_assert $CRM_EX_OK desc="Clear out constraints generated by --move" cmd="crm_resource -r dummy --clear" test_assert $CRM_EX_OK desc="Default ticket granted state" cmd="crm_ticket -t ticketA -G granted -d false" test_assert $CRM_EX_OK desc="Set ticket granted state" cmd="crm_ticket -t ticketA -r --force" test_assert $CRM_EX_OK desc="Query ticket granted state" cmd="crm_ticket -t ticketA -G granted" test_assert $CRM_EX_OK desc="Delete ticket granted state" cmd="crm_ticket -t ticketA -D granted --force" test_assert $CRM_EX_OK desc="Make a ticket standby" cmd="crm_ticket -t ticketA -s" test_assert $CRM_EX_OK desc="Query ticket standby state" cmd="crm_ticket -t ticketA -G standby" test_assert $CRM_EX_OK desc="Activate a ticket" cmd="crm_ticket -t ticketA -a" test_assert $CRM_EX_OK desc="Delete ticket standby state" cmd="crm_ticket -t ticketA -D standby" test_assert $CRM_EX_OK desc="Ban a resource on unknown node" cmd="crm_resource -r dummy -B -N host1" test_assert $CRM_EX_NOSUCH desc="Create two more nodes and bring them online" cmd="crm_simulate --live-check --in-place --node-up=node2 --node-up=node3" test_assert $CRM_EX_OK desc="Ban dummy from node1" cmd="crm_resource -r dummy -B -N node1" test_assert $CRM_EX_OK desc="Ban dummy from node2" cmd="crm_resource -r dummy -B -N node2" test_assert $CRM_EX_OK desc="Relocate resources due to ban" cmd="crm_simulate --live-check --in-place -S" test_assert $CRM_EX_OK desc="Move dummy to node1" cmd="crm_resource -r dummy -M -N node1" test_assert $CRM_EX_OK desc="Clear implicit constraints for dummy on node2" cmd="crm_resource -r dummy -U -N node2" test_assert $CRM_EX_OK desc="Drop the status section" cmd="cibadmin -R -o status --xml-text ''" test_assert $CRM_EX_OK 0 desc="Create a clone" cmd="cibadmin -C -o resources --xml-text ''" test_assert $CRM_EX_OK 0 desc="Create a resource meta attribute" cmd="crm_resource -r test-primitive --meta -p is-managed -v false" test_assert $CRM_EX_OK desc="Create a resource meta attribute in the primitive" cmd="crm_resource -r test-primitive --meta -p is-managed -v false --force" test_assert $CRM_EX_OK desc="Update resource meta attribute with duplicates" cmd="crm_resource -r test-clone --meta -p is-managed -v true" test_assert $CRM_EX_OK desc="Update resource meta attribute with duplicates (force clone)" cmd="crm_resource -r test-clone --meta -p is-managed -v true --force" test_assert $CRM_EX_OK desc="Update child resource meta attribute with duplicates" cmd="crm_resource -r test-primitive --meta -p is-managed -v false" test_assert $CRM_EX_OK desc="Delete resource meta attribute with duplicates" cmd="crm_resource -r test-clone --meta -d is-managed" test_assert $CRM_EX_OK desc="Delete resource meta attribute in parent" cmd="crm_resource -r test-primitive --meta -d is-managed" test_assert $CRM_EX_OK desc="Create a resource meta attribute in the primitive" cmd="crm_resource -r test-primitive --meta -p is-managed -v false --force" test_assert $CRM_EX_OK desc="Update existing resource meta attribute" cmd="crm_resource -r test-clone --meta -p is-managed -v true" test_assert $CRM_EX_OK desc="Create a resource meta attribute in the parent" cmd="crm_resource -r test-clone --meta -p is-managed -v true --force" test_assert $CRM_EX_OK desc="Copy resources" cmd="cibadmin -Q -o resources > $TMPXML" test_assert $CRM_EX_OK 0 desc="Delete resource paremt meta attribute (force)" cmd="crm_resource -r test-clone --meta -d is-managed --force" test_assert $CRM_EX_OK desc="Restore duplicates" cmd="cibadmin -R -o resources --xml-file $TMPXML" test_assert $CRM_EX_OK desc="Delete resource child meta attribute" cmd="crm_resource -r test-primitive --meta -d is-managed" test_assert $CRM_EX_OK desc="Specify a lifetime when moving a resource" cmd="crm_resource -r dummy --move --node node2 --lifetime=PT1H" test_assert $CRM_EX_OK desc="Try to move a resource previously moved with a lifetime" cmd="crm_resource -r dummy --move --node node1" test_assert $CRM_EX_OK desc="Ban dummy from node1 for a short time" cmd="crm_resource -r dummy -B -N node1 --lifetime=PT1S" test_assert $CRM_EX_OK desc="Remove expired constraints" sleep 2 cmd="crm_resource --clear --expired" test_assert $CRM_EX_OK unset CIB_shadow_dir rm -f "$TMPXML" "$TMPORIG" } function test_dates() { desc="2014-01-01 00:30:00 - 1 Hour" cmd="iso8601 -d '2014-01-01 00:30:00Z' -D P-1H -E '2013-12-31 23:30:00Z'" test_assert $CRM_EX_OK 0 for y in 06 07 08 09 10 11 12 13 14 15 16 17 18; do desc="20$y-W01-7" cmd="iso8601 -d '20$y-W01-7 00Z'" test_assert $CRM_EX_OK 0 desc="20$y-W01-7 - round-trip" cmd="iso8601 -d '20$y-W01-7 00Z' -W -E '20$y-W01-7 00:00:00Z'" test_assert $CRM_EX_OK 0 desc="20$y-W01-1" cmd="iso8601 -d '20$y-W01-1 00Z'" test_assert $CRM_EX_OK 0 desc="20$y-W01-1 - round-trip" cmd="iso8601 -d '20$y-W01-1 00Z' -W -E '20$y-W01-1 00:00:00Z'" test_assert $CRM_EX_OK 0 done desc="2009-W53-07" cmd="iso8601 -d '2009-W53-7 00:00:00Z' -W -E '2009-W53-7 00:00:00Z'" test_assert $CRM_EX_OK 0 desc="2009-01-31 + 1 Month" cmd="iso8601 -d '2009-01-31 00:00:00Z' -D P1M -E '2009-02-28 00:00:00Z'" test_assert $CRM_EX_OK 0 desc="2009-01-31 + 2 Months" cmd="iso8601 -d '2009-01-31 00:00:00Z' -D P2M -E '2009-03-31 00:00:00Z'" test_assert $CRM_EX_OK 0 desc="2009-01-31 + 3 Months" cmd="iso8601 -d '2009-01-31 00:00:00Z' -D P3M -E '2009-04-30 00:00:00Z'" test_assert $CRM_EX_OK 0 desc="2009-03-31 - 1 Month" cmd="iso8601 -d '2009-03-31 00:00:00Z' -D P-1M -E '2009-02-28 00:00:00Z'" test_assert $CRM_EX_OK 0 } function test_acl_loop() { local TMPXML TMPXML="$1" # Make sure we're rejecting things for the right reasons - export PCMK_trace_functions=pcmk__check_acl,pcmk__post_process_acl + export PCMK_trace_functions=pcmk__check_acl,pcmk__apply_creation_acl export PCMK_stderr=1 CIB_user=root cibadmin --replace --xml-text '' export CIB_user=unknownguy desc="$CIB_user: Query configuration" cmd="cibadmin -Q" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 desc="$CIB_user: Set enable-acl" cmd="crm_attribute -n enable-acl -v false" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 desc="$CIB_user: Set stonith-enabled" cmd="crm_attribute -n stonith-enabled -v false" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 desc="$CIB_user: Create a resource" cmd="cibadmin -C -o resources --xml-text ''" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 export CIB_user=l33t-haxor desc="$CIB_user: Query configuration" cmd="cibadmin -Q" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 desc="$CIB_user: Set enable-acl" cmd="crm_attribute -n enable-acl -v false" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 desc="$CIB_user: Set stonith-enabled" cmd="crm_attribute -n stonith-enabled -v false" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 desc="$CIB_user: Create a resource" cmd="cibadmin -C -o resources --xml-text ''" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 export CIB_user=niceguy desc="$CIB_user: Query configuration" cmd="cibadmin -Q" test_assert $CRM_EX_OK 0 desc="$CIB_user: Set enable-acl" cmd="crm_attribute -n enable-acl -v false" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 desc="$CIB_user: Set stonith-enabled" cmd="crm_attribute -n stonith-enabled -v false" test_assert $CRM_EX_OK desc="$CIB_user: Create a resource" cmd="cibadmin -C -o resources --xml-text ''" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 export CIB_user=root desc="$CIB_user: Query configuration" cmd="cibadmin -Q" test_assert $CRM_EX_OK 0 desc="$CIB_user: Set stonith-enabled" cmd="crm_attribute -n stonith-enabled -v true" test_assert $CRM_EX_OK desc="$CIB_user: Create a resource" cmd="cibadmin -C -o resources --xml-text ''" test_assert $CRM_EX_OK export CIB_user=l33t-haxor desc="$CIB_user: Create a resource meta attribute" cmd="crm_resource -r dummy --meta -p target-role -v Stopped" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 desc="$CIB_user: Query a resource meta attribute" cmd="crm_resource -r dummy --meta -g target-role" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 desc="$CIB_user: Remove a resource meta attribute" cmd="crm_resource -r dummy --meta -d target-role" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 export CIB_user=niceguy desc="$CIB_user: Create a resource meta attribute" cmd="crm_resource -r dummy --meta -p target-role -v Stopped" test_assert $CRM_EX_OK desc="$CIB_user: Query a resource meta attribute" cmd="crm_resource -r dummy --meta -g target-role" test_assert $CRM_EX_OK desc="$CIB_user: Remove a resource meta attribute" cmd="crm_resource -r dummy --meta -d target-role" test_assert $CRM_EX_OK desc="$CIB_user: Create a resource meta attribute" cmd="crm_resource -r dummy --meta -p target-role -v Started" test_assert $CRM_EX_OK export CIB_user=badidea desc="$CIB_user: Query configuration - implied deny" cmd="cibadmin -Q" test_assert $CRM_EX_OK 0 export CIB_user=betteridea desc="$CIB_user: Query configuration - explicit deny" cmd="cibadmin -Q" test_assert $CRM_EX_OK 0 CIB_user=root cibadmin -Q > "$TMPXML" CIB_user=root CIB_file="$TMPXML" CIB_shadow="" cibadmin --delete --xml-text '' CIB_user=root CIB_file="$TMPXML" CIB_shadow="" cibadmin -Ql export CIB_user=niceguy desc="$CIB_user: Replace - remove acls" cmd="cibadmin --replace --xml-file $TMPXML" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 CIB_user=root cibadmin -Q > "$TMPXML" CIB_user=root CIB_file="$TMPXML" CIB_shadow="" cibadmin -C -o resources --xml-text '' CIB_user=root CIB_file="$TMPXML" CIB_shadow="" cibadmin -Ql desc="$CIB_user: Replace - create resource" cmd="cibadmin --replace --xml-file $TMPXML" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 CIB_user=root cibadmin -Q > "$TMPXML" CIB_user=root CIB_file="$TMPXML" CIB_shadow="" crm_attribute -n enable-acl -v false CIB_user=root CIB_file="$TMPXML" CIB_shadow="" cibadmin -Ql desc="$CIB_user: Replace - modify attribute (deny)" cmd="cibadmin --replace --xml-file $TMPXML" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 CIB_user=root cibadmin -Q > "$TMPXML" CIB_user=root CIB_file="$TMPXML" CIB_shadow="" cibadmin --replace --xml-text '' CIB_user=root CIB_file="$TMPXML" CIB_shadow="" cibadmin -Ql desc="$CIB_user: Replace - delete attribute (deny)" cmd="cibadmin --replace --xml-file $TMPXML" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 CIB_user=root cibadmin -Q > "$TMPXML" CIB_user=root CIB_file="$TMPXML" CIB_shadow="" cibadmin --modify --xml-text '' CIB_user=root CIB_file="$TMPXML" CIB_shadow="" cibadmin -Ql desc="$CIB_user: Replace - create attribute (deny)" cmd="cibadmin --replace --xml-file $TMPXML" test_assert $CRM_EX_INSUFFICIENT_PRIV 0 CIB_user=bob CIB_user=root cibadmin -Q > "$TMPXML" CIB_user=root CIB_file="$TMPXML" CIB_shadow="" cibadmin --modify --xml-text '' CIB_user=root CIB_file="$TMPXML" CIB_shadow="" cibadmin -Ql desc="$CIB_user: Replace - create attribute (allow)" cmd="cibadmin --replace -o resources --xml-file $TMPXML" test_assert $CRM_EX_OK 0 CIB_user=root cibadmin -Q > "$TMPXML" CIB_user=root CIB_file="$TMPXML" CIB_shadow="" cibadmin --modify --xml-text '' CIB_user=root CIB_file="$TMPXML" CIB_shadow="" cibadmin -Ql desc="$CIB_user: Replace - modify attribute (allow)" cmd="cibadmin --replace -o resources --xml-file $TMPXML" test_assert $CRM_EX_OK 0 CIB_user=root cibadmin -Q > "$TMPXML" CIB_user=root CIB_file="$TMPXML" CIB_shadow="" cibadmin --replace -o resources --xml-text '' CIB_user=root CIB_file="$TMPXML" CIB_shadow="" cibadmin -Ql desc="$CIB_user: Replace - delete attribute (allow)" cmd="cibadmin --replace -o resources --xml-file $TMPXML" test_assert $CRM_EX_OK 0 } function test_acls() { local SHADOWPATH local TMPXML TMPXML=$(mktemp ${TMPDIR:-/tmp}/cts-cli.acls.xml.XXXXXXXXXX) export CIB_shadow_dir="${shadow_dir}" $VALGRIND_CMD crm_shadow --batch --force --create-empty $shadow --validate-with pacemaker-1.3 2>&1 export CIB_shadow=$shadow cat < "$TMPXML" EOF desc="Configure some ACLs" cmd="cibadmin -M -o acls --xml-file $TMPXML" test_assert $CRM_EX_OK desc="Enable ACLs" cmd="crm_attribute -n enable-acl -v true" test_assert $CRM_EX_OK desc="Set cluster option" cmd="crm_attribute -n no-quorum-policy -v ignore" test_assert $CRM_EX_OK desc="New ACL" cmd="cibadmin --create -o acls --xml-text ''" test_assert $CRM_EX_OK desc="Another ACL" cmd="cibadmin --create -o acls --xml-text ''" test_assert $CRM_EX_OK desc="Updated ACL" cmd="cibadmin --replace -o acls --xml-text ''" test_assert $CRM_EX_OK test_acl_loop "$TMPXML" printf "\n\n !#!#!#!#! Upgrading to latest CIB schema and re-testing !#!#!#!#!\n" printf "\nUpgrading to latest CIB schema and re-testing\n" 1>&2 export CIB_user=root desc="$CIB_user: Upgrade to latest CIB schema" cmd="cibadmin --upgrade --force -V" test_assert $CRM_EX_OK SHADOWPATH="$(crm_shadow --file)" # sed -i isn't portable :-( cp -p "$SHADOWPATH" "${SHADOWPATH}.$$" # to keep permissions sed -e 's/epoch=.2/epoch=\"6/g' -e 's/admin_epoch=.1/admin_epoch=\"0/g' \ "$SHADOWPATH" > "${SHADOWPATH}.$$" mv -- "${SHADOWPATH}.$$" "$SHADOWPATH" test_acl_loop "$TMPXML" unset CIB_shadow_dir rm -f "$TMPXML" } function test_validity() { local TMPGOOD local TMPBAD TMPGOOD=$(mktemp ${TMPDIR:-/tmp}/cts-cli.validity.good.xml.XXXXXXXXXX) TMPBAD=$(mktemp ${TMPDIR:-/tmp}/cts-cli.validity.bad.xml.XXXXXXXXXX) export CIB_shadow_dir="${shadow_dir}" $VALGRIND_CMD crm_shadow --batch --force --create-empty $shadow --validate-with pacemaker-1.2 2>&1 export CIB_shadow=$shadow export PCMK_trace_functions=apply_upgrade,update_validation,cli_config_update export PCMK_stderr=1 cibadmin -C -o resources --xml-text '' cibadmin -C -o resources --xml-text '' cibadmin -C -o constraints --xml-text '' cibadmin -Q > "$TMPGOOD" desc="Try to make resulting CIB invalid (enum violation)" cmd="cibadmin -M -o constraints --xml-text ''" test_assert $CRM_EX_CONFIG sed 's|"start"|"break"|' "$TMPGOOD" > "$TMPBAD" desc="Run crm_simulate with invalid CIB (enum violation)" cmd="crm_simulate -x $TMPBAD -S" test_assert $CRM_EX_CONFIG 0 desc="Try to make resulting CIB invalid (unrecognized validate-with)" cmd="cibadmin -M --xml-text ''" test_assert $CRM_EX_CONFIG sed 's|"pacemaker-1.2"|"pacemaker-9999.0"|' "$TMPGOOD" > "$TMPBAD" desc="Run crm_simulate with invalid CIB (unrecognized validate-with)" cmd="crm_simulate -x $TMPBAD -S" test_assert $CRM_EX_CONFIG 0 desc="Try to make resulting CIB invalid, but possibly recoverable (valid with X.Y+1)" cmd="cibadmin -C -o configuration --xml-text ''" test_assert $CRM_EX_CONFIG sed 's|||' "$TMPGOOD" > "$TMPBAD" desc="Run crm_simulate with invalid, but possibly recoverable CIB (valid with X.Y+1)" cmd="crm_simulate -x $TMPBAD -S" test_assert $CRM_EX_OK 0 sed 's|[ ][ ]*validate-with="[^"]*"||' "$TMPGOOD" > "$TMPBAD" desc="Make resulting CIB valid, although without validate-with attribute" cmd="cibadmin -R --xml-file $TMPBAD" test_assert $CRM_EX_OK desc="Run crm_simulate with valid CIB, but without validate-with attribute" cmd="crm_simulate -x $TMPBAD -S" test_assert $CRM_EX_OK 0 # this will just disable validation and accept the config, outputting # validation errors sed -e 's|[ ][ ]*validate-with="[^"]*"||' \ -e 's|\([ ][ ]*epoch="[^"]*\)"|\10"|' -e 's|"start"|"break"|' \ "$TMPGOOD" > "$TMPBAD" desc="Make resulting CIB invalid, and without validate-with attribute" cmd="cibadmin -R --xml-file $TMPBAD" test_assert $CRM_EX_OK desc="Run crm_simulate with invalid CIB, also without validate-with attribute" cmd="crm_simulate -x $TMPBAD -S" test_assert $CRM_EX_OK 0 unset CIB_shadow_dir rm -f "$TMPGOOD" "$TMPBAD" } test_upgrade() { local TMPXML TMPXML=$(mktemp ${TMPDIR:-/tmp}/cts-cli.tools.xml.XXXXXXXXXX) export CIB_shadow_dir="${shadow_dir}" $VALGRIND_CMD crm_shadow --batch --force --create-empty $shadow --validate-with pacemaker-2.10 2>&1 export CIB_shadow=$shadow desc="Set stonith-enabled=false" cmd="crm_attribute -n stonith-enabled -v false" test_assert $CRM_EX_OK cat < "$TMPXML" EOF desc="Configure the initial resource" cmd="cibadmin -M -o resources --xml-file $TMPXML" test_assert $CRM_EX_OK desc="Upgrade to latest CIB schema (trigger 2.10.xsl + the wrapping)" cmd="cibadmin --upgrade --force -V -V" test_assert $CRM_EX_OK desc="Query a resource instance attribute (shall survive)" cmd="crm_resource -r mySmartFuse -g requires" test_assert $CRM_EX_OK unset CIB_shadow_dir rm -f "$TMPXML" } test_rules() { local TMPXML export CIB_shadow_dir="${shadow_dir}" $VALGRIND_CMD crm_shadow --batch --force --create-empty $shadow 2>&1 export CIB_shadow=$shadow cibadmin -C -o resources --xml-text '' cibadmin -C -o constraints --xml-text '' TMPXML=$(mktemp ${TMPDIR:-/tmp}/cts-cli.tools.xml.XXXXXXXXXX) cat < "$TMPXML" EOF cibadmin -C -o constraints -x "$TMPXML" rm -f "$TMPXML" if [ "$(uname)" == "FreeBSD" ]; then tomorrow=$(date -v+1d +"%F %T %z") else tomorrow=$(date --date=tomorrow +"%F %T %z") fi TMPXML=$(mktemp ${TMPDIR:-/tmp}/cts-cli.tools.xml.XXXXXXXXXX) cat < "$TMPXML" EOF cibadmin -C -o constraints -x "$TMPXML" rm -f "$TMPXML" desc="Try to check a rule that doesn't exist" cmd="crm_rule -c -r blahblah" test_assert $CRM_EX_NOSUCH desc="Try to check a rule that has no date_expression" cmd="crm_rule -c -r no-date-expression" test_assert $CRM_EX_NOSUCH desc="Verify basic rule is expired" cmd="crm_rule -c -r cli-prefer-rule-dummy-expired" test_assert $CRM_EX_EXPIRED desc="Verify basic rule worked in the past" cmd="crm_rule -c -r cli-prefer-rule-dummy-expired -d 20180101" test_assert $CRM_EX_OK desc="Verify basic rule is not yet in effect" cmd="crm_rule -c -r cli-prefer-rule-dummy-not-yet" test_assert $CRM_EX_NOT_YET_IN_EFFECT unset CIB_shadow_dir } # Process command-line arguments while [ $# -gt 0 ]; do case "$1" in -t) tests="$2" shift 2 ;; -V|--verbose) verbose=1 shift ;; -v|--valgrind) export G_SLICE=always-malloc VALGRIND_CMD="valgrind $VALGRIND_OPTS" shift ;; -s) do_save=1 shift ;; -p) export PATH="$2:$PATH" shift ;; --help) echo "$USAGE_TEXT" exit $CRM_EX_OK ;; *) echo "error: unknown option $1" echo echo "$USAGE_TEXT" exit $CRM_EX_USAGE ;; esac done for t in $tests; do case "$t" in dates) ;; tools) ;; acls) ;; validity) ;; upgrade) ;; rules) ;; *) echo "error: unknown test $t" echo echo "$USAGE_TEXT" exit $CRM_EX_USAGE ;; esac done # Check whether we're running from source directory SRCDIR=$(dirname $test_home) if [ -x "$SRCDIR/tools/crm_simulate" ]; then export PATH="$SRCDIR/tools:$PATH" echo "Using local binaries from: $SRCDIR/tools" if [ -x "$SRCDIR/xml" ]; then export PCMK_schema_directory="$SRCDIR/xml" echo "Using local schemas from: $PCMK_schema_directory" fi fi for t in $tests; do echo "Testing $t" TMPFILE=$(mktemp ${TMPDIR:-/tmp}/cts-cli.$t.XXXXXXXXXX) eval TMPFILE_$t="$TMPFILE" test_$t > "$TMPFILE" sed -e 's/cib-last-written.*>/>/'\ -e 's/ last-run=\"[0-9]*\"//'\ -e 's/crm_feature_set="[^"]*" //'\ -e 's/validate-with="[^"]*" //'\ -e 's/Created new pacemaker-.* configuration/Created new pacemaker configuration/'\ -e 's/.*\(pcmk__.*\)@.*\.c:[0-9][0-9]*)/\1/g' \ -e 's/.*\(unpack_.*\)@.*\.c:[0-9][0-9]*)/\1/g' \ -e 's/.*\(update_validation\)@.*\.c:[0-9][0-9]*)/\1/g' \ -e 's/.*\(apply_upgrade\)@.*\.c:[0-9][0-9]*)/\1/g' \ -e 's/ last-rc-change=\"[0-9]*\"//'\ -e 's|^/tmp/cts-cli\.validity\.bad.xml\.[^:]*:|validity.bad.xml:|'\ -e 's/^Entity: line [0-9][0-9]*: //'\ -e 's/\(validation ([0-9][0-9]* of \)[0-9][0-9]*\().*\)/\1X\2/' \ -e 's/^Migration will take effect until: .*/Migration will take effect until:/' \ -e 's/ end=\"[0-9][-+: 0-9]*Z*\"/ end=\"\"/' \ -e 's/ start=\"[0-9][-+: 0-9]*Z*\"/ start=\"\"/' \ -e 's/^Error checking rule: Device not configured/Error checking rule: No such device or address/' \ "$TMPFILE" > "${TMPFILE}.$$" mv -- "${TMPFILE}.$$" "$TMPFILE" if [ $do_save -eq 1 ]; then cp "$TMPFILE" $test_home/cli/regression.$t.exp fi done rm -rf "${shadow_dir}" failed=0 if [ $verbose -eq 1 ]; then echo -e "\n\nResults" fi for t in $tests; do eval TMPFILE="\$TMPFILE_$t" if [ $verbose -eq 1 ]; then diff -wu $test_home/cli/regression.$t.exp "$TMPFILE" else diff -w $test_home/cli/regression.$t.exp "$TMPFILE" >/dev/null 2>&1 fi if [ $? -ne 0 ]; then failed=1 fi done echo -e "\n\nSummary" for t in $tests; do eval TMPFILE="\$TMPFILE_$t" grep -e '^\*' "$TMPFILE" done if [ $num_errors -ne 0 ]; then echo "$num_errors tests failed; see output in:" for t in $tests; do eval TMPFILE="\$TMPFILE_$t" echo " $TMPFILE" done exit $CRM_EX_ERROR elif [ $failed -eq 1 ]; then echo "$num_passed tests passed but output was unexpected; see output in:" for t in $tests; do eval TMPFILE="\$TMPFILE_$t" echo " $TMPFILE" done exit $CRM_EX_DIGEST else echo $num_passed tests passed for t in $tests; do eval TMPFILE="\$TMPFILE_$t" rm -f "$TMPFILE" done crm_shadow --force --delete $shadow >/dev/null 2>&1 exit $CRM_EX_OK fi diff --git a/doc/Pacemaker_Explained/en-US/Book_Info.xml b/doc/Pacemaker_Explained/en-US/Book_Info.xml index 3da61de4f7..aae3d06b4d 100644 --- a/doc/Pacemaker_Explained/en-US/Book_Info.xml +++ b/doc/Pacemaker_Explained/en-US/Book_Info.xml @@ -1,35 +1,35 @@ Configuration Explained An A-Z guide to Pacemaker's Configuration Options Pacemaker 2.0 12 - 0 + 2 The purpose of this document is to definitively explain the concepts used to configure Pacemaker. To achieve this, it will focus exclusively on the XML syntax used to configure Pacemaker's Cluster Information Base (CIB). diff --git a/doc/Pacemaker_Explained/en-US/Ch-ACLs.txt b/doc/Pacemaker_Explained/en-US/Ch-ACLs.txt new file mode 100644 index 0000000000..178a2949e6 --- /dev/null +++ b/doc/Pacemaker_Explained/en-US/Ch-ACLs.txt @@ -0,0 +1,334 @@ +:compat-mode: legacy += Access Control Lists (ACLs) = + +//// +We prefer [[ch-acls]], but older versions of asciidoc don't deal well +with that construct for chapter headings +//// +anchor:ch-acls[Chapter 13, ACLs] +indexterm:[access control list] +indexterm:[ACL] + +By default, the +root+ user or any user in the +haclient+ group can modify +Pacemaker's CIB without restriction. Pacemaker offers 'access control lists +(ACLs)' to provide more fine-grained authorization. + +== ACL Prerequisites == + +In order to use ACLs: + +* The Pacemaker software must have been compiled with ACL support. If the + output of the command `pacemakerd --features` contains `acls`, your + installation supports ACLs. + +* Desired users must have user accounts in the +haclient+ group on all nodes in + the cluster. + +* If your CIB was created before Pacemaker 1.1.12, it may need to be updated to + the current schema using `cibadmin --upgrade` in order to use the syntax + documented here. + +* The +enable-acl+ <> must be set to true. + +== ACL Configuration == + +ACLs are specified within an +acls+ element of the CIB. The +acls+ element may +contain any number of +acl_role+, +acl_target+, and +acl_group+ elements. + +== ACL Roles == + +An ACL role is a collection of permissions allowing or denying access to +particular portions of the CIB. + +.Properties of an ACL Role +[width="95%",cols="1m,<3",options="header",align="center"] +|==== + +|Attribute +|Description + +|id +|A unique name for the role (required) + indexterm:[id,acl_role] + indexterm:[access control list,acl_role,id] + +|description +|Arbitrary text (not used by Pacemaker) + indexterm:[description,acl_role] + indexterm:[access control list,acl_role,description] + +|==== + +An +acl_role+ element may contain any number of +acl_permission+ elements. + +.Properties of an ACL Permission +[width="95%",cols="1m,<3",options="header",align="center"] +|==== + +|Attribute +|Description + +|id +|A unique name for the permission (required) + indexterm:[id,acl_permission] + indexterm:[access control list,acl_permission,id] + +|description +|Arbitrary text (not used by Pacemaker) + indexterm:[description,acl_permission] + indexterm:[access control list,acl_permission,description] + +|kind +|The access being granted. Allowed values are +read+, +write+, and +deny+. + A value of +write+ grants both read and write access. + indexterm:[kind,acl_permission] + indexterm:[access control list,acl_permission,kind] + +|object-type +|The name of an XML element in the CIB to which the permission applies. + (Exactly one of +object-type+, +xpath+, and +reference+ must be specified for + a permission.) + indexterm:[object-type,acl_permission] + indexterm:[access control list,acl_permission,object-type] + +|attribute +|If specified, the permission applies only to +object-type+ elements that have + this attribute set (to any value). If not specified, the permission applies to + all +object-type+ elements. May only be used with +object-type+. + indexterm:[attribute,acl_permission] + indexterm:[access control list,acl_permission,attribute] + +|reference +|The ID of an XML element in the CIB to which the permission applies. + (Exactly one of +object-type+, +xpath+, and +reference+ must be specified for + a permission.) + indexterm:[reference,acl_permission] + indexterm:[access control list,acl_permission,reference] + +|xpath +|An https://www.w3.org/TR/xpath-10/[XPath] specification selecting an XML + element in the CIB to which the permission applies. Attributes may be + specified in the XPath to select particular elements, but the permissions + apply to the entire element. + (Exactly one of +object-type+, +xpath+, and +reference+ must be specified for + a permission.) + indexterm:[xpath,acl_permission] + indexterm:[access control list,acl_permission,xpath] + +|==== + +[IMPORTANT] +==== +* Permissions are applied to the selected XML element's entire XML subtree + (all elements enclosed within it). + +* Write permission grants the ability to create, modify, or remove the element + and its subtree, and also the ability to create any "scaffolding" elements + (enclosing elements that do not have attributes other than an ID). + +* Permissions for more specific matches (more deeply nested elements) take + precedence over more general ones. + +* If multiple permissions are configured for the same match (for example, in + different roles applied to the same user), any +deny+ permission takes + precedence, then +write+, then lastly +read+. +==== + +== ACL Targets and Groups == + +ACL targets correspond to user accounts on the system. + +.Properties of an ACL Target +[width="95%",cols="1m,<3",options="header",align="center"] +|==== + +|Attribute +|Description + +|id +|The name of a user on the system (required) + indexterm:[id,acl_target] + indexterm:[access control list,acl_target,id] + +|==== + +ACL groups may be specified, but are not currently used by Pacemaker. This is +expected to change in a future version. + +.Properties of an ACL Group +[width="95%",cols="1m,<3",options="header",align="center"] +|==== + +|Attribute +|Description + +|id +|The name of a group on the system (required) + indexterm:[id,acl_group] + indexterm:[access control list,acl_group,id] + +|==== + +Each +acl_target+ and +acl_group+ element may contain any number of +role+ +elements. + +.Properties of an ACL Role Reference +[width="95%",cols="1m,<3",options="header",align="center"] +|==== + +|Attribute +|Description + +|id +|The +id+ of an +acl_role+ element that specifies permissions granted to the + enclosing target or group + indexterm:[id,role] + indexterm:[access control list,role,id] + +|==== + +[IMPORTANT] +==== +The +root+ and +hacluster+ user accounts always have full access to the CIB, +regardless of ACLs. For other user accounts, when +enable-acl+ is true, +permission to all parts of the CIB is denied by default (permissions must be +explicitly granted). +==== + +== ACL Examples == + +[source,XML] +---- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +---- + +In the above example, the user +alice+ has the minimal permissions necessary to +run basic Pacemaker CLI tools, including using `crm_mon` to view the cluster +status, without being able to modify anything. The user +bob+ can view the +entire configuration and status of the cluster, but not make any changes. The +user +carol+ can read everything, and change selected cluster properties as +well as resource roles and location constraints. Finally, +dave+ has full read +and write access to the entire CIB. + +Looking at the +minimal+ role in more depth, it is designed to allow read +access to the +cib+ tag itself, while denying access to particular portions of +its subtree (which is the entire CIB). + +This is because the DC node is indicated in the +cib+ tag, so `crm_mon` will +not be able to report the DC otherwise. However, this does change the security +model to allow by default, since any portions of the CIB not explicitly denied +will be readable. The +cib+ read access could be removed and replaced with read +access to just the +crm_config+ and +status+ sections, for a safer approach at +the cost of not seeing the DC in status output. + +For a simpler configuration, the +minimal+ role allows read access to the +entire +crm_config+ section, which contains cluster properties. It would be +possible to allow read access to specific properties instead (such as ++stonith-enabled+, +dc-uuid+, +have-quorum+, and +cluster-name+) to restrict +access further while still allowing status output, but cluster properties are +unlikely to be considered sensitive. diff --git a/doc/Pacemaker_Explained/en-US/Ch-Options.txt b/doc/Pacemaker_Explained/en-US/Ch-Options.txt index 3fb06a96e3..15001b1265 100644 --- a/doc/Pacemaker_Explained/en-US/Ch-Options.txt +++ b/doc/Pacemaker_Explained/en-US/Ch-Options.txt @@ -1,422 +1,428 @@ :compat-mode: legacy = Cluster-Wide Configuration = == Configuration Layout == The cluster is defined by the Cluster Information Base (CIB), which uses XML notation. The simplest CIB, an empty one, looks like this: .An empty configuration ====== [source,XML] ------- ------- ====== The empty configuration above contains the major sections that make up a CIB: * +cib+: The entire CIB is enclosed with a +cib+ tag. Certain fundamental settings are defined as attributes of this tag. ** +configuration+: This section -- the primary focus of this document -- contains traditional configuration information such as what resources the cluster serves and the relationships among them. *** +crm_config+: cluster-wide configuration options *** +nodes+: the machines that host the cluster *** +resources+: the services run by the cluster *** +constraints+: indications of how resources should be placed ** +status+: This section contains the history of each resource on each node. Based on this data, the cluster can construct the complete current state of the cluster. The authoritative source for this section is the local executor (pacemaker-execd process) on each cluster node, and the cluster will occasionally repopulate the entire section. For this reason, it is never written to disk, and administrators are advised against modifying it in any way. In this document, configuration settings will be described as 'properties' or 'options' based on how they are defined in the CIB: * Properties are XML attributes of an XML element. * Options are name-value pairs expressed as +nvpair+ child elements of an XML element. Normally, you will use command-line tools that abstract the XML, so the distinction will be unimportant; both properties and options are cluster settings you can tweak. == CIB Properties == Certain settings are defined by CIB properties (that is, attributes of the +cib+ tag) rather than with the rest of the cluster configuration in the +configuration+ section. The reason is simply a matter of parsing. These options are used by the configuration database which is, by design, mostly ignorant of the content it holds. So the decision was made to place them in an easy-to-find location. .CIB Properties [width="95%",cols="2m,<5",options="header",align="center"] |========================================================= |Field |Description | admin_epoch | indexterm:[Configuration Version,Cluster] indexterm:[Cluster,Option,Configuration Version] indexterm:[admin_epoch,Cluster Option] indexterm:[Cluster,Option,admin_epoch] When a node joins the cluster, the cluster performs a check to see which node has the best configuration. It asks the node with the highest (+admin_epoch+, +epoch+, +num_updates+) tuple to replace the configuration on all the nodes -- which makes setting them, and setting them correctly, very important. +admin_epoch+ is never modified by the cluster; you can use this to make the configurations on any inactive nodes obsolete. _Never set this value to zero_. In such cases, the cluster cannot tell the difference between your configuration and the "empty" one used when nothing is found on disk. | epoch | indexterm:[epoch,Cluster Option] indexterm:[Cluster,Option,epoch] The cluster increments this every time the configuration is updated (usually by the administrator). | num_updates | indexterm:[num_updates,Cluster Option] indexterm:[Cluster,Option,num_updates] The cluster increments this every time the configuration or status is updated (usually by the cluster) and resets it to 0 when epoch changes. | validate-with | indexterm:[validate-with,Cluster Option] indexterm:[Cluster,Option,validate-with] Determines the type of XML validation that will be done on the configuration. If set to +none+, the cluster will not verify that updates conform to the DTD (nor reject ones that don't). This option can be useful when operating a mixed-version cluster during an upgrade. |cib-last-written | indexterm:[cib-last-written,Cluster Property] indexterm:[Cluster,Property,cib-last-written] Indicates when the configuration was last written to disk. Maintained by the cluster; for informational purposes only. |have-quorum | indexterm:[have-quorum,Cluster Property] indexterm:[Cluster,Property,have-quorum] Indicates if the cluster has quorum. If false, this may mean that the cluster cannot start resources or fence other nodes (see +no-quorum-policy+ below). Maintained by the cluster. |dc-uuid | indexterm:[dc-uuid,Cluster Property] indexterm:[Cluster,Property,dc-uuid] Indicates which cluster node is the current leader. Used by the cluster when placing resources and determining the order of some events. Maintained by the cluster. |========================================================= [[s-cluster-options]] == Cluster Options == Cluster options, as you might expect, control how the cluster behaves when confronted with certain situations. They are grouped into sets within the +crm_config+ section, and, in advanced configurations, there may be more than one set. (This will be described later in the section on <> where we will show how to have the cluster use different sets of options during working hours than during weekends.) For now, we will describe the simple case where each option is present at most once. You can obtain an up-to-date list of cluster options, including their default values, by running the `man pacemaker-schedulerd` and `man pacemaker-controld` commands. .Cluster Options [width="95%",cols="5m,2,<11",options="header",align="center"] |========================================================= |Option |Default |Description | cluster-name | | indexterm:[cluster-name,Cluster Property] indexterm:[Cluster,Property,cluster-name] An (optional) name for the cluster as a whole. This is mostly for users' convenience for use as desired in administration, but this can be used in the Pacemaker configuration in <> (as the +#cluster-name+ <>). It may also be used by higher-level tools when displaying cluster information, and by certain resource agents (for example, the +ocf:heartbeat:GFS2+ agent stores the cluster name in filesystem meta-data). | dc-version | | indexterm:[dc-version,Cluster Property] indexterm:[Cluster,Property,dc-version] Version of Pacemaker on the cluster's DC. Determined automatically by the cluster. Often includes the hash which identifies the exact Git changeset it was built from. Used for diagnostic purposes. | cluster-infrastructure | | indexterm:[cluster-infrastructure,Cluster Property] indexterm:[Cluster,Property,cluster-infrastructure] The messaging stack on which Pacemaker is currently running. Determined automatically by the cluster. Used for informational and diagnostic purposes. | no-quorum-policy | stop a| indexterm:[no-quorum-policy,Cluster Option] indexterm:[Cluster,Option,no-quorum-policy] What to do when the cluster does not have quorum. Allowed values: * +ignore:+ continue all resource management * +freeze:+ continue resource management, but don't recover resources from nodes not in the affected partition * +stop:+ stop all resources in the affected cluster partition * +suicide:+ fence all nodes in the affected cluster partition | batch-limit | 0 | indexterm:[batch-limit,Cluster Option] indexterm:[Cluster,Option,batch-limit] The maximum number of actions that the cluster may execute in parallel across all nodes. The "correct" value will depend on the speed and load of your network and cluster nodes. If zero, the cluster will impose a dynamically calculated limit only when any node has high load. | migration-limit | -1 | indexterm:[migration-limit,Cluster Option] indexterm:[Cluster,Option,migration-limit] The number of <> actions that the cluster is allowed to execute in parallel on a node. A value of -1 means unlimited. | symmetric-cluster | TRUE | indexterm:[symmetric-cluster,Cluster Option] indexterm:[Cluster,Option,symmetric-cluster] Can all resources run on any node by default? | stop-all-resources | FALSE | indexterm:[stop-all-resources,Cluster Option] indexterm:[Cluster,Option,stop-all-resources] Should the cluster stop all resources? | stop-orphan-resources | TRUE | indexterm:[stop-orphan-resources,Cluster Option] indexterm:[Cluster,Option,stop-orphan-resources] Should deleted resources be stopped? This value takes precedence over +is-managed+ (i.e. even unmanaged resources will be stopped if deleted from the configuration when this value is TRUE). | stop-orphan-actions | TRUE | indexterm:[stop-orphan-actions,Cluster Option] indexterm:[Cluster,Option,stop-orphan-actions] Should deleted actions be cancelled? | start-failure-is-fatal | TRUE | indexterm:[start-failure-is-fatal,Cluster Option] indexterm:[Cluster,Option,start-failure-is-fatal] Should a failure to start a resource on a particular node prevent further start attempts on that node? If FALSE, the cluster will decide whether the same node is still eligible based on the resource's current failure count and +migration-threshold+ (see <>). | enable-startup-probes | TRUE | indexterm:[enable-startup-probes,Cluster Option] indexterm:[Cluster,Option,enable-startup-probes] Should the cluster check for active resources during startup? | maintenance-mode | FALSE | indexterm:[maintenance-mode,Cluster Option] indexterm:[Cluster,Option,maintenance-mode] Should the cluster refrain from monitoring, starting and stopping resources? | stonith-enabled | TRUE | indexterm:[stonith-enabled,Cluster Option] indexterm:[Cluster,Option,stonith-enabled] Should failed nodes and nodes with resources that can't be stopped be shot? If you value your data, set up a STONITH device and enable this. If true, or unset, the cluster will refuse to start resources unless one or more STONITH resources have been configured. If false, unresponsive nodes are immediately assumed to be running no resources, and resource takeover to online nodes starts without any further protection (which means _data loss_ if the unresponsive node still accesses shared storage, for example). See also the +requires+ meta-attribute in <>. | stonith-action | reboot | indexterm:[stonith-action,Cluster Option] indexterm:[Cluster,Option,stonith-action] Action to send to STONITH device. Allowed values are +reboot+ and +off+. The value +poweroff+ is also allowed, but is only used for legacy devices. | stonith-timeout | 60s | indexterm:[stonith-timeout,Cluster Option] indexterm:[Cluster,Option,stonith-timeout] How long to wait for STONITH actions (reboot, on, off) to complete | stonith-max-attempts | 10 | indexterm:[stonith-max-attempts,Cluster Option] indexterm:[Cluster,Option,stonith-max-attempts] How many times fencing can fail for a target before the cluster will no longer immediately re-attempt it. | stonith-watchdog-timeout | 0 | indexterm:[stonith-watchdog-timeout,Cluster Option] indexterm:[Cluster,Option,stonith-watchdog-timeout] If nonzero, rely on hardware watchdog self-fencing. If positive, assume unseen nodes self-fence within this much time. If negative, and the SBD_WATCHDOG_TIMEOUT environment variable is set, use twice that value. | concurrent-fencing | FALSE | indexterm:[concurrent-fencing,Cluster Option] indexterm:[Cluster,Option,concurrent-fencing] Is the cluster allowed to initiate multiple fence actions concurrently? | cluster-delay | 60s | indexterm:[cluster-delay,Cluster Option] indexterm:[Cluster,Option,cluster-delay] Estimated maximum round-trip delay over the network (excluding action execution). If the DC requires an action to be executed on another node, it will consider the action failed if it does not get a response from the other node in this time (after considering the action's own timeout). The "correct" value will depend on the speed and load of your network and cluster nodes. | dc-deadtime | 20s | indexterm:[dc-deadtime,Cluster Option] indexterm:[Cluster,Option,dc-deadtime] How long to wait for a response from other nodes during startup. The "correct" value will depend on the speed/load of your network and the type of switches used. | cluster-recheck-interval | 15min | indexterm:[cluster-recheck-interval,Cluster Option] indexterm:[Cluster,Option,cluster-recheck-interval] Polling interval for time-based changes to options, resource parameters and constraints. The Cluster is primarily event-driven, but your configuration can have elements that take effect based on the time of day. To ensure these changes take effect, we can optionally poll the cluster's status for changes. A value of 0 disables polling. Positive values are an interval (in seconds unless other SI units are specified, e.g. 5min). | cluster-ipc-limit | 500 | indexterm:[cluster-ipc-limit,Cluster Option] indexterm:[Cluster,Option,cluster-ipc-limit] The maximum IPC message backlog before one cluster daemon will disconnect another. This is of use in large clusters, for which a good value is the number of resources in the cluster multiplied by the number of nodes. The default of 500 is also the minimum. Raise this if you see "Evicting client" messages for cluster daemon PIDs in the logs. | pe-error-series-max | -1 | indexterm:[pe-error-series-max,Cluster Option] indexterm:[Cluster,Option,pe-error-series-max] The number of PE inputs resulting in ERRORs to save. Used when reporting problems. A value of -1 means unlimited (report all). | pe-warn-series-max | -1 | indexterm:[pe-warn-series-max,Cluster Option] indexterm:[Cluster,Option,pe-warn-series-max] The number of PE inputs resulting in WARNINGs to save. Used when reporting problems. A value of -1 means unlimited (report all). | pe-input-series-max | -1 | indexterm:[pe-input-series-max,Cluster Option] indexterm:[Cluster,Option,pe-input-series-max] The number of "normal" PE inputs to save. Used when reporting problems. A value of -1 means unlimited (report all). | placement-strategy | default | indexterm:[placement-strategy,Cluster Option] indexterm:[Cluster,Option,placement-strategy] How the cluster should allocate resources to nodes (see <>). Allowed values are +default+, +utilization+, +balanced+, and +minimal+. | node-health-strategy | none | indexterm:[node-health-strategy,Cluster Option] indexterm:[Cluster,Option,node-health-strategy] How the cluster should react to node health attributes (see <>). Allowed values are +none+, +migrate-on-red+, +only-green+, +progressive+, and +custom+. +| enable-acl | FALSE | +indexterm:[enable-acl,Cluster Option] +indexterm:[Cluster,Option,enable-acl] + Whether access control lists (ACLs) (see <>) can be used to authorize + modifications to the CIB. + | node-health-base | 0 | indexterm:[node-health-base,Cluster Option] indexterm:[Cluster,Option,node-health-base] The base health score assigned to a node. Only used when +node-health-strategy+ is +progressive+. | node-health-green | 0 | indexterm:[node-health-green,Cluster Option] indexterm:[Cluster,Option,node-health-green] The score to use for a node health attribute whose value is +green+. Only used when +node-health-strategy+ is +progressive+ or +custom+. | node-health-yellow | 0 | indexterm:[node-health-yellow,Cluster Option] indexterm:[Cluster,Option,node-health-yellow] The score to use for a node health attribute whose value is +yellow+. Only used when +node-health-strategy+ is +progressive+ or +custom+. | node-health-red | 0 | indexterm:[node-health-red,Cluster Option] indexterm:[Cluster,Option,node-health-red] The score to use for a node health attribute whose value is +red+. Only used when +node-health-strategy+ is +progressive+ or +custom+. | remove-after-stop | FALSE | indexterm:[remove-after-stop,Cluster Option] indexterm:[Cluster,Option,remove-after-stop] _Advanced Use Only:_ Should the cluster remove resources from the LRM after they are stopped? Values other than the default are, at best, poorly tested and potentially dangerous. | startup-fencing | TRUE | indexterm:[startup-fencing,Cluster Option] indexterm:[Cluster,Option,startup-fencing] _Advanced Use Only:_ Should the cluster shoot unseen nodes? Not using the default is very unsafe! | election-timeout | 2min | indexterm:[election-timeout,Cluster Option] indexterm:[Cluster,Option,election-timeout] _Advanced Use Only:_ If you need to adjust this value, it probably indicates the presence of a bug. | shutdown-escalation | 20min | indexterm:[shutdown-escalation,Cluster Option] indexterm:[Cluster,Option,shutdown-escalation] _Advanced Use Only:_ If you need to adjust this value, it probably indicates the presence of a bug. | join-integration-timeout | 3min | indexterm:[join-integration-timeout,Cluster Option] indexterm:[Cluster,Option,join-integration-timeout] _Advanced Use Only:_ If you need to adjust this value, it probably indicates the presence of a bug. | join-finalization-timeout | 30min | indexterm:[join-finalization-timeout,Cluster Option] indexterm:[Cluster,Option,join-finalization-timeout] _Advanced Use Only:_ If you need to adjust this value, it probably indicates the presence of a bug. | transition-delay | 0s | indexterm:[transition-delay,Cluster Option] indexterm:[Cluster,Option,transition-delay] _Advanced Use Only:_ Delay cluster recovery for the configured interval to allow for additional/related events to occur. Useful if your configuration is sensitive to the order in which ping updates arrive. Enabling this option will slow down cluster recovery under all conditions. |========================================================= diff --git a/doc/Pacemaker_Explained/en-US/Pacemaker_Explained.xml b/doc/Pacemaker_Explained/en-US/Pacemaker_Explained.xml index 31bb67675d..5c3c84b7a4 100644 --- a/doc/Pacemaker_Explained/en-US/Pacemaker_Explained.xml +++ b/doc/Pacemaker_Explained/en-US/Pacemaker_Explained.xml @@ -1,26 +1,27 @@ + diff --git a/doc/Pacemaker_Explained/en-US/Revision_History.xml b/doc/Pacemaker_Explained/en-US/Revision_History.xml index 270734f3ea..531b3760f5 100644 --- a/doc/Pacemaker_Explained/en-US/Revision_History.xml +++ b/doc/Pacemaker_Explained/en-US/Revision_History.xml @@ -1,174 +1,186 @@ Revision History 1-0 19 Oct 2009 AndrewBeekhofandrew@beekhof.net Import from Pages.app 2-0 26 Oct 2009 AndrewBeekhofandrew@beekhof.net Cleanup and reformatting of docbook xml complete 3-0 Tue Nov 12 2009 AndrewBeekhofandrew@beekhof.net Split book into chapters and pass validation Re-organize book for use with Publican 4-0 Mon Oct 8 2012 AndrewBeekhofandrew@beekhof.net Converted to asciidoc (which is converted to docbook for use with Publican) 5-0 Mon Feb 23 2015 KenGaillotkgaillot@redhat.com Update for clarity, stylistic consistency and current command-line syntax 6-0 Tue Dec 8 2015 KenGaillotkgaillot@redhat.com Update for Pacemaker 1.1.14 7-0 Tue May 3 2016 KenGaillotkgaillot@redhat.com Update for Pacemaker 1.1.15 7-1 Fri Oct 28 2016 KenGaillotkgaillot@redhat.com Overhaul upgrade documentation, and document node health strategies 8-0 Tue Oct 25 2016 KenGaillotkgaillot@redhat.com Update for Pacemaker 1.1.16 9-0 Tue Jul 11 2017 KenGaillotkgaillot@redhat.com Update for Pacemaker 1.1.17 10-0 Fri Oct 6 2017 KenGaillotkgaillot@redhat.com Update for Pacemaker 1.1.18 11-0 Fri Jan 12 2018 KenGaillotkgaillot@redhat.com Update for Pacemaker 2.0.0 12-0 Mon Jan 28 2019 KenGaillotkgaillot@redhat.com ReidWahlnwahl@redhat.com JanPokornýjpokorny@redhat.com Update for Pacemaker 2.0.1, remove "Further Reading" and "FAQ" sections, and add minor clarifications and reformatting 12-1 Mon May 13 2019 KenGaillotkgaillot@redhat.com MaciejSobkowiakmsobkowiak@egnyte.com Document podman support, cluster-name cluster option, and new HealthIOWait agent, with other minor clarifications and corrections + + 12-2 + Wed Jun 19 2019 + KenGaillotkgaillot@redhat.com + + + + Add chapter for ACLs + + + + diff --git a/doc/acls.txt b/doc/acls.txt index ce3d44f381..2028856aeb 100644 --- a/doc/acls.txt +++ b/doc/acls.txt @@ -1,200 +1,4 @@ Pacemaker Access Control Lists ============================== -Tim Serong - -== Introduction - -The various tools for administering Pacemaker clusters (crm_mon, -crm shell, cibadmin and friends, Python GUI, Hawk) can be used by -the +root+ user, or any user in the +haclient+ group. By default, -these users have full read/write access. Starting with Pacemaker -version 1.1.5, flexible access control lists are introduced to -provide different levels of administration to different users. - -== Prerequisites - -* Users are regular UNIX users, so the same user accounts must - be present on all nodes in the cluster. -* All user accounts must be in the +haclient+ group. -* Pacemaker 1.1.5 or newer must be installed on all cluster nodes. -* The CIB must be configured to use the pacemaker-1.1 or 1.2 schema. - This can be set by running: - - cibadmin --modify --xml-text '' - -* The +enable-acl+ option must be set. If ACLs are not explicitly - enabled, the previous behaviour will be used (i.e. all users in - the +haclient+ group have full access): - - crm configure property enable-acl=true - -* Once this is done, ACLs can be configured as described below. -* Note that the +root+ and +hacluster+ users will always have full - access. -* If nonprivileged users will be using the crm shell and CLI tools - (as opposed to only using Hawk or the Python GUI) they will need - to have +/usr/sbin+ added to their path. - -== Configuring ACLs - -Access control lists consist of an ordered set of access rules. -Each rule allows read or write access or denies access completely -to a part of the CIB. Rules are typically combined to produce a -specific role, then users may be assigned to that role. It is -also possible to configure ACLs directly for individual users. - -ACLs may be configured using the crm shell or the Python GUI. -The shell syntax is documented here. Using the Python GUI should -be reasonably straightforward once the concepts are understood. - -Note that rules are applied from first to last with the first -matching rule being used. This means specific +write+ rules -usually need to be listed before general +read+ rules. Any -permission not explicitly granted is denied by default, but note -that +write+ implies +read+, so there is no need to specify both -to allow full read/write access. - -=== Minimum Required ACLs - -In order for the various tools to work correctly, a certain minimum -amount of data must be readable by the user invoking the tool. In -general, the safest thing to do is simply allow all users and roles -read access to the entire CIB. - -=== Role Syntax - -An ACL role is a set of rules which describe access rights to -CIB. Rules consist of an access right +read+, +write+, or +deny+ -and a specification denoting part of the configuration to which -the access right applies. The specification can be an XPath or a -combination of tag and id references. If an attribute is appended, -then the specification applies only to that attribute of the matching -element. - -==== Usage - -............... - role rule [rule ...] - - rule :: acl-right cib-spec [attribute:] - - acl-right :: read | write | deny - - cib-spec :: xpath-spec | tag-ref-spec - xpath-spec :: xpath: - tag-ref-spec :: tag: | ref: | tag: ref: -............... - -==== Example Role: Read-only Access - -............... - role monitor \ - read xpath:"/cib" -............... - -This is a single rule which allows read-only access to the entire -CIB. Users with this role will be able to view the status of -the cluster, but not make any changes. - -==== Example Role: ``Operator'' Access - -............... - role operator \ - write xpath:"//crm_config//nvpair[@name='maintenance-mode']" \ - write xpath:"//op_defaults//nvpair[@name='record-pending']" \ - write xpath:"//nodes/node//nvpair[@name='standby']" \ - write xpath:"//resources//nvpair[@name='target-role']" \ - write xpath:"//resources//nvpair[@name='is-managed']" \ - write xpath:"//constraints/rsc_location" \ - read xpath:"/cib" -............... - -These rules specify that users with this role will be able to: - -. Turn maintenance mode on or off -. Change whether pending operations are recorded -. Put any node on standby, and bring any node back online -. Start, stop, promote or demote any resource -. Tell the cluster to manage, or not manage any resource -. Migrate/move resources from node to note -. View the status of the cluster - -Users with this role will not be able to reconfigure resources -(change parameters, operations, etc.) or colocation or ordering -constraints. - -==== Example Role: Full Access - -............... - role administrator \ - write xpath:"/cib" -............... - -This is a single rule which allows full read-write access to the -entire CIB. Users with this role will have equivalent access to -the +root+ and +hacluster+ users. - -=== User Syntax - -ACLs can be defined for individual users using the same syntax -as for roles. Alternately, users can simply be assigned a given -role. The latter is considered best practice. - -==== Usage - -............... - user {role:|rule [rule ...]} - - rule :: acl-right cib-spec [attribute:] - - acl-right :: read | write | deny - - cib-spec :: xpath-spec | tag-ref-spec - xpath-spec :: xpath: - tag-ref-spec :: tag: | ref: | tag: ref: -............... - -==== Example - -............... - user alice role:monitor - user bob role:operator -............... - -The above example assigns +alice+ the +monitor+ role and +bob+ the -+operator+ role. - -== Advanced Usage - -Because ACLs can refer to elements and attributes in the CIB in a -very granular way, it is possible to configure very specific rules. -A refinement of the ``operator'' role above would be to allow -manipulation of only a specific resource, for example: - -............... - role bigdb_admin \ - write xpath:"//primitive[@id='bigdb']/meta_attributes/nvpair[@name='target-role']" \ - write xpath:"//primitive[@id='bigdb']/meta_attributes/nvpair[@name='is-managed']" \ - write xpath:"//constraints/rsc_location[@rsc='bigdb']" \ - read ref:"bigdb" \ - read xpath:"//nodes/node" uname \ - read xpath:"//nodes/node" type \ - read xpath:"//crm_config/cluster_property_set" \ - read xpath:"/cib/status" -............... - -The first four rules are specific for the +bigdb+ resource. They -allow modifying the +target-role+ and +is-managed+ meta -attributes which effectively enables users in this role to -stop/start and manage/unmanage the resource. The constraints -write access rule allows moving the resource around. Finally, the -user is granted read access to the resource definition. - -The bottom four rules are the absolute minimum read permissions -necessary for proper operation of various Pacemaker tools, -including `crm_mon` and the shell. This is fine for viewing -cluster status, but there are some tools for which this will -not be sufficient access (notably `crm_simulate`), which is why it is -generally recommended that read access be allowed to the entire -CIB. - +This document has been retired. See the Access Control Lists chapter of the +Pacemaker Explained documentation instead. diff --git a/include/crm/common/xml.h b/include/crm/common/xml.h index 4754a38a0c..565b3105fd 100644 --- a/include/crm/common/xml.h +++ b/include/crm/common/xml.h @@ -1,321 +1,322 @@ /* * Copyright 2004-2019 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. */ #ifndef CRM_COMMON_XML__H # define CRM_COMMON_XML__H #ifdef __cplusplus extern "C" { #endif /** * \file * \brief Wrappers for and extensions to libxml2 * \ingroup core */ # include # include # include # include # include # include # include # include # include # include /* Define compression parameters for IPC messages * * Compression costs a LOT, so we don't want to do it unless we're hitting * message limits. Currently, we use 128KB as the threshold, because higher * values don't play well with the heartbeat stack. With an earlier limit of * 10KB, compressing 184 of 1071 messages accounted for 23% of the total CPU * used by the cib. */ # define CRM_BZ2_BLOCKS 4 # define CRM_BZ2_WORK 20 # define CRM_BZ2_THRESHOLD 128 * 1024 # define XML_PARANOIA_CHECKS 0 typedef const xmlChar *pcmkXmlStr; gboolean add_message_xml(xmlNode * msg, const char *field, xmlNode * xml); xmlNode *get_message_xml(xmlNode * msg, const char *field); xmlDoc *getDocPtr(xmlNode * node); /* * Replacement function for xmlCopyPropList which at the very least, * doesn't work the way *I* would expect it to. * * Copy all the attributes/properties from src into target. * * Not recursive, does not return anything. * */ void copy_in_properties(xmlNode * target, xmlNode * src); void expand_plus_plus(xmlNode * target, const char *name, const char *value); void fix_plus_plus_recursive(xmlNode * target); /* * Create a node named "name" as a child of "parent" * If parent is NULL, creates an unconnected node. * * Returns the created node * */ xmlNode *create_xml_node(xmlNode * parent, const char *name); /* * */ void purge_diff_markers(xmlNode * a_node); /* * Returns a deep copy of src_node * */ xmlNode *copy_xml(xmlNode * src_node); /* * Add a copy of xml_node to new_parent */ xmlNode *add_node_copy(xmlNode * new_parent, xmlNode * xml_node); int add_node_nocopy(xmlNode * parent, const char *name, xmlNode * child); /* * XML I/O Functions * * Whitespace between tags is discarded. */ xmlNode *filename2xml(const char *filename); xmlNode *stdin2xml(void); xmlNode *string2xml(const char *input); int write_xml_fd(xmlNode * xml_node, const char *filename, int fd, gboolean compress); int write_xml_file(xmlNode * xml_node, const char *filename, gboolean compress); char *dump_xml_formatted(xmlNode * msg); /* Also dump the text node with xml_log_option_text enabled */ char *dump_xml_formatted_with_text(xmlNode * msg); char *dump_xml_unformatted(xmlNode * msg); /* * Diff related Functions */ xmlNode *diff_xml_object(xmlNode * left, xmlNode * right, gboolean suppress); xmlNode *subtract_xml_object(xmlNode * parent, xmlNode * left, xmlNode * right, gboolean full, gboolean * changed, const char *marker); gboolean can_prune_leaf(xmlNode * xml_node); gboolean apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml); /* * Searching & Modifying */ xmlNode *find_xml_node(xmlNode * cib, const char *node_path, gboolean must_find); xmlNode *find_entity(xmlNode * parent, const char *node_name, const char *id); void xml_remove_prop(xmlNode * obj, const char *name); gboolean replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only); gboolean update_xml_child(xmlNode * child, xmlNode * to_update); int find_xml_children(xmlNode ** children, xmlNode * root, const char *tag, const char *field, const char *value, gboolean search_matches); xmlNode *get_xpath_object(const char *xpath, xmlNode * xml_obj, int error_level); xmlNode *get_xpath_object_relative(const char *xpath, xmlNode * xml_obj, int error_level); static inline const char * crm_element_name(const xmlNode *xml) { return xml? (const char *)(xml->name) : NULL; } gboolean xml_has_children(const xmlNode * root); char *calculate_on_disk_digest(xmlNode * local_cib); char *calculate_operation_digest(xmlNode * local_cib, const char *version); char *calculate_xml_versioned_digest(xmlNode * input, gboolean sort, gboolean do_filter, const char *version); /* schema-related functions (from schemas.c) */ gboolean validate_xml(xmlNode * xml_blob, const char *validation, gboolean to_logs); gboolean validate_xml_verbose(xmlNode * xml_blob); /*! * \brief Update CIB XML to most recent schema version * * "Update" means either actively employ XSLT-based transformation(s) * (if intermediate product to transform valid per its declared schema version, * transformation available, proceeded successfully with a result valid per * expectated newer schema version), or just try to bump the marked validating * schema until all gradually rising schema versions attested or the first * such attempt subsequently fails to validate. Which of the two styles will * be used depends on \p transform parameter (positive/negative, respectively). * * \param[in,out] xml_blob XML tree representing CIB, may be swapped with * an "updated" one * \param[out] best The highest configuration version (per its index * in the global schemas table) it was possible to * reach during the update steps while ensuring * the validity of the result; if no validation * success was observed against possibly multiple * schemas, the value is less or equal the result * of \c get_schema_version applied on the input * \p xml_blob value (unless that function maps it * to -1, then 0 would be used instead) * \param[in] max When \p transform is positive, this allows to * set upper boundary schema (per its index in the * global schemas table) beyond which it's forbidden * to update by the means of XSLT transformation * \param[in] transform Whether to employ XSLT-based transformation so * as to allow overcoming possible incompatibilities * between major schema versions (see above) * \param[in] to_logs If true, output notable progress info to * internal log streams; if false, to stderr * * \return \c pcmk_ok if no non-recoverable error encountered (up to * caller to evaluate if the update satisfies the requirements * per returned \p best value), negative value carrying the reason * otherwise */ int update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, gboolean to_logs); int get_schema_version(const char *name); const char *get_schema_name(int version); const char *xml_latest_schema(void); gboolean cli_config_update(xmlNode ** xml, int *best_version, gboolean to_logs); void crm_xml_init(void); void crm_xml_cleanup(void); static inline xmlNode * __xml_first_child(const xmlNode *parent) { xmlNode *child = parent? parent->children : NULL; while (child && (child->type == XML_TEXT_NODE)) { child = child->next; } return child; } static inline xmlNode * __xml_next(const xmlNode *child) { xmlNode *next = child? child->next : NULL; while (next && (next->type == XML_TEXT_NODE)) { next = next->next; } return next; } static inline xmlNode * __xml_first_child_element(const xmlNode *parent) { xmlNode *child = parent? parent->children : NULL; while (child && (child->type != XML_ELEMENT_NODE)) { child = child->next; } return child; } static inline xmlNode * __xml_next_element(const xmlNode *child) { xmlNode *next = child? child->next : NULL; while (next && (next->type != XML_ELEMENT_NODE)) { next = next->next; } return next; } +void pcmk_free_xml_subtree(xmlNode *xml); void free_xml(xmlNode * child); xmlNode *first_named_child(const xmlNode *parent, const char *name); xmlNode *crm_next_same_xml(const xmlNode *sibling); xmlNode *sorted_xml(xmlNode * input, xmlNode * parent, gboolean recursive); xmlXPathObjectPtr xpath_search(xmlNode * xml_top, const char *path); void crm_foreach_xpath_result(xmlNode *xml, const char *xpath, void (*helper)(xmlNode*, void*), void *user_data); xmlNode *expand_idref(xmlNode * input, xmlNode * top); void freeXpathObject(xmlXPathObjectPtr xpathObj); xmlNode *getXpathResult(xmlXPathObjectPtr xpathObj, int index); void dedupXpathResults(xmlXPathObjectPtr xpathObj); static inline int numXpathResults(xmlXPathObjectPtr xpathObj) { if(xpathObj == NULL || xpathObj->nodesetval == NULL) { return 0; } return xpathObj->nodesetval->nodeNr; } bool xml_acl_enabled(xmlNode *xml); void xml_acl_disable(xmlNode *xml); bool xml_acl_denied(xmlNode *xml); /* Part or all of a change was rejected */ bool xml_acl_filtered_copy(const char *user, xmlNode* acl_source, xmlNode *xml, xmlNode ** result); bool xml_tracking_changes(xmlNode * xml); bool xml_document_dirty(xmlNode *xml); void xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls); void xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml); void xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml); void xml_accept_changes(xmlNode * xml); void xml_log_changes(uint8_t level, const char *function, xmlNode *xml); void xml_log_patchset(uint8_t level, const char *function, xmlNode *xml); bool xml_patch_versions(xmlNode *patchset, int add[3], int del[3]); xmlNode *xml_create_patchset( int format, xmlNode *source, xmlNode *target, bool *config, bool manage_version); int xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version); void patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target, bool with_digest); void save_xml_to_file(xmlNode * xml, const char *desc, const char *filename); char *xml_get_path(xmlNode *xml); char * crm_xml_escape(const char *text); void crm_xml_sanitize_id(char *id); void crm_xml_set_id(xmlNode *xml, const char *format, ...) __attribute__ ((__format__ (__printf__, 2, 3))); /*! * \brief xmlNode destructor which can be used in glib collections */ void crm_destroy_xml(gpointer data); #ifdef __cplusplus } #endif #endif diff --git a/lib/common/acl.c b/lib/common/acl.c index 2f7cf73714..7ed4e79982 100644 --- a/lib/common/acl.c +++ b/lib/common/acl.c @@ -1,723 +1,801 @@ /* - * Copyright 2004-2018 Andrew Beekhof + * Copyright 2004-2019 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 "crmcommon_private.h" #define MAX_XPATH_LEN 4096 typedef struct xml_acl_s { enum xml_private_flags mode; char *xpath; } xml_acl_t; static void __xml_acl_free(void *data) { if (data) { xml_acl_t *acl = data; free(acl->xpath); free(acl); } } void pcmk__free_acls(GList *acls) { g_list_free_full(acls, __xml_acl_free); } static GList * __xml_acl_create(xmlNode *xml, GList *acls, enum xml_private_flags mode) { xml_acl_t *acl = NULL; const char *tag = crm_element_value(xml, XML_ACL_ATTR_TAG); const char *ref = crm_element_value(xml, XML_ACL_ATTR_REF); const char *xpath = crm_element_value(xml, XML_ACL_ATTR_XPATH); + const char *attr = crm_element_value(xml, XML_ACL_ATTR_ATTRIBUTE); if (tag == NULL) { // @COMPAT rolling upgrades <=1.1.11 tag = crm_element_value(xml, XML_ACL_ATTR_TAGv1); } if (ref == NULL) { // @COMPAT rolling upgrades <=1.1.11 ref = crm_element_value(xml, XML_ACL_ATTR_REFv1); } if ((tag == NULL) && (ref == NULL) && (xpath == NULL)) { - crm_trace("No criteria %p", xml); + // Schema should prevent this, but to be safe ... + crm_trace("Ignoring ACL <%s> element without selection criteria", + crm_element_name(xml)); return NULL; } acl = calloc(1, sizeof (xml_acl_t)); - if (acl) { - const char *attr = crm_element_value(xml, XML_ACL_ATTR_ATTRIBUTE); + CRM_ASSERT(acl != NULL); - acl->mode = mode; - if (xpath) { - acl->xpath = strdup(xpath); - crm_trace("Using xpath: %s", acl->xpath); + acl->mode = mode; + if (xpath) { + acl->xpath = strdup(xpath); + CRM_ASSERT(acl->xpath != NULL); + crm_trace("Unpacked ACL <%s> element using xpath: %s", + crm_element_name(xml), acl->xpath); - } else { - int offset = 0; - char buffer[MAX_XPATH_LEN]; - - if (tag) { - offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset, - "//%s", tag); - } else { - offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset, - "//*"); - } + } else { + int offset = 0; + char buffer[MAX_XPATH_LEN]; - if (ref || attr) { - offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset, - "["); - } + if (tag) { + offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset, + "//%s", tag); + } else { + offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset, + "//*"); + } - if (ref) { - offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset, - "@id='%s'", ref); - } + if (ref || attr) { + offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset, + "["); + } - if (ref && attr) { - offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset, - " and "); - } + if (ref) { + offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset, + "@id='%s'", ref); + } - if (attr) { - offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset, - "@%s", attr); - } + // NOTE: schema currently does not allow this + if (ref && attr) { + offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset, + " and "); + } - if (ref || attr) { - offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset, - "]"); - } + if (attr) { + offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset, + "@%s", attr); + } - CRM_LOG_ASSERT(offset > 0); - acl->xpath = strdup(buffer); - crm_trace("Built xpath: %s", acl->xpath); + if (ref || attr) { + offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset, + "]"); } - acls = g_list_append(acls, acl); + CRM_LOG_ASSERT(offset > 0); + acl->xpath = strdup(buffer); + CRM_ASSERT(acl->xpath != NULL); + + crm_trace("Unpacked ACL <%s> element as xpath: %s", + crm_element_name(xml), acl->xpath); } - return acls; + + 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 + */ static GList * __xml_acl_parse_entry(xmlNode *acl_top, xmlNode *acl_entry, GList *acls) { xmlNode *child = NULL; for (child = __xml_first_child(acl_entry); child; child = __xml_next(child)) { const char *tag = crm_element_name(child); const char *kind = crm_element_value(child, XML_ACL_ATTR_KIND); if (strcmp(XML_ACL_TAG_PERMISSION, tag) == 0){ + CRM_ASSERT(kind != NULL); + crm_trace("Unpacking ACL <%s> element of kind '%s'", tag, kind); tag = kind; + } else { + crm_trace("Unpacking ACL <%s> element", tag); } - crm_trace("Processing %s %p", tag, child); - if (tag == NULL) { - CRM_ASSERT(tag != NULL); - - } else if (strcmp(XML_ACL_TAG_ROLE_REF, tag) == 0 + if (strcmp(XML_ACL_TAG_ROLE_REF, tag) == 0 || strcmp(XML_ACL_TAG_ROLE_REFv1, tag) == 0) { const char *ref_role = crm_element_value(child, XML_ATTR_ID); if (ref_role) { xmlNode *role = NULL; for (role = __xml_first_child(acl_top); role; role = __xml_next(role)) { if (!strcmp(XML_ACL_TAG_ROLE, (const char *) role->name)) { const char *role_id = crm_element_value(role, XML_ATTR_ID); if (role_id && strcmp(ref_role, role_id) == 0) { - crm_debug("Unpacking referenced role: %s", role_id); + crm_trace("Unpacking referenced role '%s' in ACL <%s> element", + role_id, crm_element_name(acl_entry)); acls = __xml_acl_parse_entry(acl_top, role, acls); break; } } } } } else if (strcmp(XML_ACL_TAG_READ, tag) == 0) { acls = __xml_acl_create(child, acls, xpf_acl_read); } else if (strcmp(XML_ACL_TAG_WRITE, tag) == 0) { acls = __xml_acl_create(child, acls, xpf_acl_write); } else if (strcmp(XML_ACL_TAG_DENY, tag) == 0) { acls = __xml_acl_create(child, acls, xpf_acl_deny); } else { - crm_warn("Unknown ACL entry: %s/%s", tag, kind); + crm_warn("Ignoring unknown ACL %s '%s'", + (kind? "kind" : "element"), tag); } } return acls; } /* */ -#ifdef SUSE_ACL_COMPAT static const char * __xml_acl_to_text(enum xml_private_flags flags) { if (is_set(flags, xpf_acl_deny)) { return "deny"; - } else if (is_set(flags, xpf_acl_write)) { + } else if (is_set(flags, xpf_acl_write) || is_set(flags, xpf_acl_create)) { return "read/write"; } else if (is_set(flags, xpf_acl_read)) { return "read"; } return "none"; } -#endif void pcmk__apply_acl(xmlNode *xml) { GListPtr aIter = NULL; xml_private_t *p = xml->doc->_private; xmlXPathObjectPtr xpathObj = NULL; if (xml_acl_enabled(xml) == FALSE) { - crm_trace("Not applying ACLs for %s", p->user); + crm_trace("Skipping ACLs for user '%s' because not enabled for this XML", + p->user); return; } for (aIter = p->acls; aIter != NULL; aIter = aIter->next) { int max = 0, lpc = 0; xml_acl_t *acl = aIter->data; xpathObj = xpath_search(xml, acl->xpath); max = numXpathResults(xpathObj); for (lpc = 0; lpc < max; lpc++) { xmlNode *match = getXpathResult(xpathObj, lpc); char *path = xml_get_path(match); p = match->_private; - crm_trace("Applying %x to %s for %s", acl->mode, path, acl->xpath); + crm_trace("Applying %s ACL to %s matched by %s", + __xml_acl_to_text(acl->mode), path, acl->xpath); #ifdef SUSE_ACL_COMPAT if (is_not_set(p->flags, acl->mode) && (is_set(p->flags, xpf_acl_read) || is_set(p->flags, xpf_acl_write) || is_set(p->flags, xpf_acl_deny))) { crm_config_warn("Configuration element %s is matched by " "multiple ACL rules, only the first applies " "('%s' wins over '%s')", path, __xml_acl_to_text(p->flags), __xml_acl_to_text(acl->mode)); free(path); continue; } #endif p->flags |= acl->mode; free(path); } - crm_trace("Now enforcing ACL: %s (%d matches)", acl->xpath, max); + crm_trace("Applied %s ACL %s (%d match%s)", + __xml_acl_to_text(acl->mode), acl->xpath, max, + ((max == 1)? "" : "es")); freeXpathObject(xpathObj); } p = xml->_private; if (is_not_set(p->flags, xpf_acl_read) && is_not_set(p->flags, xpf_acl_write)) { p->flags |= xpf_acl_deny; p = xml->doc->_private; - crm_info("Enforcing default ACL for %s to %s", + crm_info("Applied default deny ACL for user '%s' to <%s>", p->user, crm_element_name(xml)); } } +/*! + * \internal + * \brief Unpack ACLs for a given user + * + * \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) { #if ENABLE_ACL xml_private_t *p = NULL; if ((target == NULL) || (target->doc == NULL) || (target->doc->_private == NULL)) { return; } p = target->doc->_private; if (pcmk_acl_required(user) == FALSE) { - crm_trace("no acls needed for '%s'", user); + crm_trace("Not unpacking ACLs because not required for user '%s'", + user); } else if (p->acls == NULL) { xmlNode *acls = get_xpath_object("//" XML_CIB_TAG_ACLS, source, LOG_TRACE); free(p->user); p->user = strdup(user); if (acls) { xmlNode *child = NULL; for (child = __xml_first_child(acls); child; child = __xml_next(child)) { const char *tag = crm_element_name(child); if (!strcmp(tag, XML_ACL_TAG_USER) || !strcmp(tag, XML_ACL_TAG_USERv1)) { const char *id = crm_element_value(child, XML_ATTR_ID); if (id && strcmp(id, user) == 0) { - crm_debug("Unpacking ACLs for %s", id); + crm_debug("Unpacking ACLs for user '%s'", id); p->acls = __xml_acl_parse_entry(acls, child, p->acls); } } } } } #endif } static inline bool __xml_acl_mode_test(enum xml_private_flags allowed, enum xml_private_flags requested) { if (is_set(allowed, xpf_acl_deny)) { return FALSE; } else if (is_set(allowed, requested)) { return TRUE; } else if (is_set(requested, xpf_acl_read) && is_set(allowed, xpf_acl_write)) { return TRUE; } else if (is_set(requested, xpf_acl_create) && is_set(allowed, xpf_acl_write)) { return TRUE; } else if (is_set(requested, xpf_acl_create) && is_set(allowed, xpf_created)) { return TRUE; } return FALSE; } /* rc = TRUE if orig_cib has been filtered * That means '*result' rather than 'xml' should be exploited afterwards */ static bool __xml_purge_attributes(xmlNode *xml) { xmlNode *child = NULL; xmlAttr *xIter = NULL; bool readable_children = FALSE; xml_private_t *p = xml->_private; if (__xml_acl_mode_test(p->flags, xpf_acl_read)) { crm_trace("%s[@id=%s] is readable", crm_element_name(xml), 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, XML_ATTR_ID) == 0) { continue; } xmlUnsetProp(xml, tmp->name); } child = __xml_first_child(xml); while ( child != NULL ) { xmlNode *tmp = child; child = __xml_next(child); readable_children |= __xml_purge_attributes(tmp); } if (readable_children == FALSE) { free_xml(xml); /* Nothing readable under here, purge completely */ } return readable_children; } +/*! + * \internal + * \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, otherwise FALSE + */ bool xml_acl_filtered_copy(const char *user, xmlNode *acl_source, xmlNode *xml, xmlNode **result) { GListPtr aIter = NULL; xmlNode *target = NULL; xml_private_t *p = NULL; xml_private_t *doc = NULL; *result = NULL; if (xml == NULL || pcmk_acl_required(user) == FALSE) { - crm_trace("no acls needed for '%s'", user); + crm_trace("Not filtering XML because ACLs not required for user '%s'", + user); return FALSE; } - crm_trace("filtering copy of %p for '%s'", xml, user); + crm_trace("Filtering XML copy using user '%s' ACLs", user); target = copy_xml(xml); if (target == NULL) { return TRUE; } pcmk__unpack_acl(acl_source, target, user); pcmk__set_xml_flag(target, xpf_acl_enabled); pcmk__apply_acl(target); doc = target->doc->_private; for(aIter = doc->acls; aIter != NULL && target; aIter = aIter->next) { int max = 0; xml_acl_t *acl = aIter->data; if (acl->mode != xpf_acl_deny) { /* Nothing to do */ } else if (acl->xpath) { int lpc = 0; xmlXPathObjectPtr xpathObj = xpath_search(target, acl->xpath); max = numXpathResults(xpathObj); for(lpc = 0; lpc < max; lpc++) { xmlNode *match = getXpathResult(xpathObj, lpc); - crm_trace("Purging attributes from %s", acl->xpath); if (__xml_purge_attributes(match) == FALSE && match == target) { - crm_trace("No access to the entire document for %s", user); + crm_trace("ACLs deny user '%s' access to entire XML document", + user); freeXpathObject(xpathObj); return TRUE; } } - crm_trace("Enforced ACL %s (%d matches)", acl->xpath, max); + crm_trace("ACLs deny user '%s' access to %s (%d match%s)", + user, acl->xpath, max, ((max == 1)? "" : "es")); freeXpathObject(xpathObj); } } p = target->_private; if (is_set(p->flags, xpf_acl_deny) && (__xml_purge_attributes(target) == FALSE)) { - crm_trace("No access to the entire document for %s", user); + crm_trace("ACLs deny user '%s' access to entire XML document", user); return TRUE; } if (doc->acls) { g_list_free_full(doc->acls, __xml_acl_free); doc->acls = NULL; } else { - crm_trace("Ordinary user '%s' cannot access the CIB without any defined ACLs", - doc->user); + crm_trace("User '%s' without ACLs denied access to entire XML document", + user); free_xml(target); target = NULL; } if (target) { *result = target; } return TRUE; } -void -pcmk__post_process_acl(xmlNode *xml) +/*! + * \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 "id"). + * + * \param[in] xml XML element to check + * + * \return TRUE if XML element is implicitly allowed, FALSE otherwise + */ +static bool +implicitly_allowed(xmlNode *xml) { - xmlNode *cIter = __xml_first_child(xml); - xml_private_t *p = xml->_private; + char *path = NULL; - if (is_set(p->flags, xpf_created)) { - xmlAttr *xIter = NULL; - char *path = xml_get_path(xml); + for (xmlAttr *prop = xml->properties; prop != NULL; prop = prop->next) { + if (strcmp((const char *) prop->name, XML_ATTR_ID) != 0) { + return FALSE; + } + } - /* Always allow new scaffolding (e.g. node with no attributes or only an - * 'id'), except in the ACLs section - */ + path = xml_get_path(xml); + if (strstr(path, "/" XML_CIB_TAG_ACLS "/") != NULL) { + free(path); + return FALSE; + } + free(path); - for (xIter = xml->properties; xIter != NULL; xIter = xIter->next) { - const char *prop_name = (const char *)xIter->name; + return TRUE; +} - if (!strcmp(prop_name, XML_ATTR_ID) - && !strstr(path, "/"XML_CIB_TAG_ACLS"/")) { - /* Delay the acl check */ - continue; +#define display_id(xml) (ID(xml)? ID(xml) : "") + +/*! + * \internal + * \brief Drop XML nodes created in violation of ACLs + * + * Given an XML element, free all of its descendent 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 + * "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) + */ +void +pcmk__apply_creation_acl(xmlNode *xml, bool check_top) +{ + xml_private_t *p = xml->_private; - } else if (pcmk__check_acl(xml, NULL, xpf_acl_write)) { - crm_trace("Creation of %s=%s is allowed", - crm_element_name(xml), ID(xml)); - break; + if (is_set(p->flags, xpf_created)) { + if (implicitly_allowed(xml)) { + crm_trace("Creation of <%s> scaffolding with id=\"%s\"" + " is implicitly allowed", + crm_element_name(xml), display_id(xml)); - } else { - crm_trace("Cannot add new node %s at %s", - crm_element_name(xml), path); + } else if (pcmk__check_acl(xml, NULL, xpf_acl_write)) { + crm_trace("ACLs allow creation of <%s> with id=\"%s\"", + crm_element_name(xml), display_id(xml)); - if (xml != xmlDocGetRootElement(xml->doc)) { - xmlUnlinkNode(xml); - xmlFreeNode(xml); - } - free(path); - return; - } + } else if (check_top) { + crm_trace("ACLs disallow creation of <%s> with id=\"%s\"", + crm_element_name(xml), display_id(xml)); + pcmk_free_xml_subtree(xml); + return; + + } else { + crm_notice("ACLs would disallow creation of %s<%s> with id=\"%s\" ", + ((xml == xmlDocGetRootElement(xml->doc))? "root element " : ""), + crm_element_name(xml), display_id(xml)); } - free(path); } - while (cIter != NULL) { + for (xmlNode *cIter = __xml_first_child(xml); cIter != NULL; ) { xmlNode *child = cIter; cIter = __xml_next(cIter); /* In case it is free'd */ - pcmk__post_process_acl(child); + pcmk__apply_creation_acl(child, TRUE); } } bool xml_acl_denied(xmlNode *xml) { if (xml && xml->doc && xml->doc->_private){ xml_private_t *p = xml->doc->_private; return is_set(p->flags, xpf_acl_denied); } return FALSE; } void xml_acl_disable(xmlNode *xml) { if (xml_acl_enabled(xml)) { xml_private_t *p = xml->doc->_private; /* Catch anything that was created but shouldn't have been */ pcmk__apply_acl(xml); - pcmk__post_process_acl(xml); + pcmk__apply_creation_acl(xml, FALSE); clear_bit(p->flags, xpf_acl_enabled); } } bool xml_acl_enabled(xmlNode *xml) { if (xml && xml->doc && xml->doc->_private){ xml_private_t *p = xml->doc->_private; return is_set(p->flags, xpf_acl_enabled); } return FALSE; } bool pcmk__check_acl(xmlNode *xml, const char *name, enum xml_private_flags mode) { CRM_ASSERT(xml); CRM_ASSERT(xml->doc); CRM_ASSERT(xml->doc->_private); #if ENABLE_ACL if (pcmk__tracking_xml_changes(xml, FALSE) && xml_acl_enabled(xml)) { int offset = 0; xmlNode *parent = xml; char buffer[MAX_XPATH_LEN]; xml_private_t *docp = xml->doc->_private; - if (docp->acls == NULL) { - crm_trace("Ordinary user %s cannot access the CIB without any defined ACLs", - docp->user); - pcmk__set_xml_flag(xml, xpf_acl_denied); - return FALSE; - } - offset = pcmk__element_xpath(NULL, xml, buffer, offset, sizeof(buffer)); if (name) { offset += snprintf(buffer + offset, MAX_XPATH_LEN - offset, "[@%s]", name); } CRM_LOG_ASSERT(offset > 0); + if (docp->acls == NULL) { + crm_trace("User '%s' without ACLs denied %s access to %s", + docp->user, __xml_acl_to_text(mode), buffer); + pcmk__set_xml_flag(xml, xpf_acl_denied); + 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 (name) { xmlAttr *attr = xmlHasProp(xml, (pcmkXmlStr) name); if (attr && mode == xpf_acl_create) { mode = xpf_acl_write; } } while (parent && parent->_private) { xml_private_t *p = parent->_private; if (__xml_acl_mode_test(p->flags, mode)) { return TRUE; } else if (is_set(p->flags, xpf_acl_deny)) { - crm_trace("%x access denied to %s: parent", mode, buffer); + crm_trace("Parent ACL denies user '%s' %s access to %s", + docp->user, __xml_acl_to_text(mode), buffer); pcmk__set_xml_flag(xml, xpf_acl_denied); return FALSE; } parent = parent->parent; } - crm_trace("%x access denied to %s: default", mode, buffer); + crm_trace("Default ACL denies user '%s' %s access to %s", + docp->user, __xml_acl_to_text(mode), buffer); pcmk__set_xml_flag(xml, xpf_acl_denied); return FALSE; } #endif return TRUE; } bool pcmk_acl_required(const char *user) { #if ENABLE_ACL - if (user == NULL || strlen(user) == 0) { - crm_trace("no user set"); - return FALSE; - - } else if (strcmp(user, CRM_DAEMON_USER) == 0) { + if ((user == NULL) || (*user == '\0')) { + crm_trace("ACLs not required because no user set"); return FALSE; - } else if (strcmp(user, "root") == 0) { + } 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; #else - crm_trace("ACLs not supported"); + crm_trace("ACLs not required because not supported by this build"); return FALSE; #endif } #if ENABLE_ACL char * uid2username(uid_t uid) { struct passwd *pwent = getpwuid(uid); if (pwent == NULL) { crm_perror(LOG_INFO, "Cannot get user details for user ID %d", uid); return NULL; } return strdup(pwent->pw_name); } const char * crm_acl_get_set_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 = uid2username(geteuid()); if (effective_user == NULL) { effective_user = strdup("#unprivileged"); CRM_CHECK(effective_user != NULL, return NULL); crm_err("Unable to determine effective user, assuming unprivileged for ACLs"); } } requested_user = crm_element_value(request, XML_ACL_TAG_USER); if (requested_user == NULL) { /* @COMPAT rolling upgrades <=1.1.11 * * field is checked for backward compatibility with older versions that * did not use XML_ACL_TAG_USER. */ requested_user = crm_element_value(request, field); } if (is_privileged(effective_user) == FALSE) { /* We're not running as a privileged user, set or overwrite any existing * value for $XML_ACL_TAG_USER */ 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 (is_privileged(peer_user) == FALSE) { /* The peer is not a privileged user, set or overwrite any existing * value for $XML_ACL_TAG_USER */ 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 != crm_element_value(request, XML_ACL_TAG_USER)) { crm_xml_add(request, XML_ACL_TAG_USER, user); } if (field != NULL && user != crm_element_value(request, field)) { crm_xml_add(request, field, user); } return requested_user; } #endif diff --git a/lib/common/crmcommon_private.h b/lib/common/crmcommon_private.h index 113f525c11..dfb1e54010 100644 --- a/lib/common/crmcommon_private.h +++ b/lib/common/crmcommon_private.h @@ -1,87 +1,89 @@ /* - * Copyright 2018-2019 Andrew Beekhof + * Copyright 2018-2019 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. */ #ifndef CRMCOMMON_PRIVATE__H # define CRMCOMMON_PRIVATE__H /* This header is for the sole use of libcrmcommon, so that functions can be * declared with G_GNUC_INTERNAL for efficiency. */ enum xml_private_flags { xpf_none = 0x0000, xpf_dirty = 0x0001, xpf_deleted = 0x0002, xpf_created = 0x0004, xpf_modified = 0x0008, xpf_tracking = 0x0010, xpf_processed = 0x0020, xpf_skip = 0x0040, xpf_moved = 0x0080, xpf_acl_enabled = 0x0100, xpf_acl_read = 0x0200, xpf_acl_write = 0x0400, xpf_acl_deny = 0x0800, xpf_acl_create = 0x1000, xpf_acl_denied = 0x2000, xpf_lazy = 0x4000, }; typedef struct xml_private_s { long check; uint32_t flags; char *user; GListPtr acls; GListPtr deleted_objs; } xml_private_t; G_GNUC_INTERNAL void pcmk__set_xml_flag(xmlNode *xml, enum xml_private_flags flag); G_GNUC_INTERNAL bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy); G_GNUC_INTERNAL int pcmk__element_xpath(const char *prefix, xmlNode *xml, char *buffer, int offset, size_t buffer_size); G_GNUC_INTERNAL void pcmk__free_acls(GList *acls); G_GNUC_INTERNAL void pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user); G_GNUC_INTERNAL bool pcmk__check_acl(xmlNode *xml, const char *name, enum xml_private_flags mode); G_GNUC_INTERNAL void pcmk__apply_acl(xmlNode *xml); G_GNUC_INTERNAL -void pcmk__post_process_acl(xmlNode *xml); +void pcmk__apply_creation_acl(xmlNode *xml, bool check_top); G_GNUC_INTERNAL void pcmk__mark_xml_attr_dirty(xmlAttr *a); static inline xmlAttr * pcmk__first_xml_attr(const xmlNode *xml) { return xml? xml->properties : NULL; } static inline const char * pcmk__xml_attr_value(const xmlAttr *attr) { return ((attr == NULL) || (attr->children == NULL))? NULL : (const char *) attr->children->content; } #endif // CRMCOMMON_PRIVATE__H diff --git a/lib/common/xml.c b/lib/common/xml.c index 940c4b91ae..bdc5dd5c62 100644 --- a/lib/common/xml.c +++ b/lib/common/xml.c @@ -1,4331 +1,4337 @@ /* * Copyright 2004-2019 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 /* CRM_XML_LOG_BASE */ #include "crmcommon_private.h" #if HAVE_BZLIB_H # include #endif #define XML_BUFFER_SIZE 4096 #define XML_PARSER_DEBUG 0 typedef struct { int found; const char *string; } filter_t; typedef struct xml_deleted_obj_s { char *path; int position; } xml_deleted_obj_t; /* *INDENT-OFF* */ static filter_t filter[] = { { 0, XML_ATTR_ORIGIN }, { 0, XML_CIB_ATTR_WRITTEN }, { 0, XML_ATTR_UPDATE_ORIG }, { 0, XML_ATTR_UPDATE_CLIENT }, { 0, XML_ATTR_UPDATE_USER }, }; /* *INDENT-ON* */ static xmlNode *subtract_xml_comment(xmlNode * parent, xmlNode * left, xmlNode * right, gboolean * changed); static xmlNode *find_xml_comment(xmlNode * root, xmlNode * search_comment, gboolean exact); static int add_xml_comment(xmlNode * parent, xmlNode * target, xmlNode * update); #define CHUNK_SIZE 1024 bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy) { if(xml == NULL || xml->doc == NULL || xml->doc->_private == NULL) { return FALSE; } else if(is_not_set(((xml_private_t *)xml->doc->_private)->flags, xpf_tracking)) { return FALSE; } else if (lazy && is_not_set(((xml_private_t *)xml->doc->_private)->flags, xpf_lazy)) { return FALSE; } return TRUE; } #define buffer_print(buffer, max, offset, fmt, args...) do { \ int rc = (max); \ if(buffer) { \ rc = snprintf((buffer) + (offset), (max) - (offset), fmt, ##args); \ } \ if(buffer && rc < 0) { \ crm_perror(LOG_ERR, "snprintf failed at offset %d", offset); \ (buffer)[(offset)] = 0; \ break; \ } else if(rc >= ((max) - (offset))) { \ char *tmp = NULL; \ (max) = QB_MAX(CHUNK_SIZE, (max) * 2); \ tmp = realloc_safe((buffer), (max)); \ CRM_ASSERT(tmp); \ (buffer) = tmp; \ } else { \ offset += rc; \ break; \ } \ } while(1); static void insert_prefix(int options, char **buffer, int *offset, int *max, int depth) { if (options & xml_log_option_formatted) { size_t spaces = 2 * depth; if ((*buffer) == NULL || spaces >= ((*max) - (*offset))) { (*max) = QB_MAX(CHUNK_SIZE, (*max) * 2); (*buffer) = realloc_safe((*buffer), (*max)); } memset((*buffer) + (*offset), ' ', spaces); (*offset) += spaces; } } static void set_parent_flag(xmlNode *xml, long flag) { for(; xml; xml = xml->parent) { xml_private_t *p = xml->_private; if(p == NULL) { /* During calls to xmlDocCopyNode(), _private will be unset for parent nodes */ } else { p->flags |= flag; /* crm_trace("Setting flag %x due to %s[@id=%s]", flag, xml->name, ID(xml)); */ } } } void pcmk__set_xml_flag(xmlNode *xml, enum xml_private_flags flag) { if(xml && xml->doc && xml->doc->_private){ /* During calls to xmlDocCopyNode(), xml->doc may be unset */ xml_private_t *p = xml->doc->_private; p->flags |= flag; /* crm_trace("Setting flag %x due to %s[@id=%s]", flag, xml->name, ID(xml)); */ } } static void __xml_node_dirty(xmlNode *xml) { pcmk__set_xml_flag(xml, xpf_dirty); set_parent_flag(xml, xpf_dirty); } static void __xml_node_clean(xmlNode *xml) { xmlNode *cIter = NULL; xml_private_t *p = xml->_private; if(p) { p->flags = 0; } for (cIter = __xml_first_child(xml); cIter != NULL; cIter = __xml_next(cIter)) { __xml_node_clean(cIter); } } static void crm_node_created(xmlNode *xml) { xmlNode *cIter = NULL; xml_private_t *p = xml->_private; if(p && pcmk__tracking_xml_changes(xml, FALSE)) { if(is_not_set(p->flags, xpf_created)) { p->flags |= xpf_created; __xml_node_dirty(xml); } for (cIter = __xml_first_child(xml); cIter != NULL; cIter = __xml_next(cIter)) { crm_node_created(cIter); } } } void pcmk__mark_xml_attr_dirty(xmlAttr *a) { xmlNode *parent = a->parent; xml_private_t *p = NULL; p = a->_private; p->flags |= (xpf_dirty|xpf_modified); p->flags = (p->flags & ~xpf_deleted); /* crm_trace("Setting flag %x due to %s[@id=%s, @%s=%s]", */ /* xpf_dirty, parent?parent->name:NULL, ID(parent), a->name, a->children->content); */ __xml_node_dirty(parent); } int get_tag_name(const char *input, size_t offset, size_t max); int get_attr_name(const char *input, size_t offset, size_t max); int get_attr_value(const char *input, size_t offset, size_t max); gboolean can_prune_leaf(xmlNode * xml_node); static int add_xml_object(xmlNode * parent, xmlNode * target, xmlNode * update, gboolean as_diff); #define XML_PRIVATE_MAGIC (long) 0x81726354 static void __xml_deleted_obj_free(void *data) { if(data) { xml_deleted_obj_t *deleted_obj = data; free(deleted_obj->path); free(deleted_obj); } } static void __xml_private_clean(xml_private_t *p) { if(p) { CRM_ASSERT(p->check == XML_PRIVATE_MAGIC); free(p->user); p->user = NULL; if(p->acls) { pcmk__free_acls(p->acls); p->acls = NULL; } if(p->deleted_objs) { g_list_free_full(p->deleted_objs, __xml_deleted_obj_free); p->deleted_objs = NULL; } } } static void __xml_private_free(xml_private_t *p) { __xml_private_clean(p); free(p); } static void pcmkDeregisterNode(xmlNodePtr node) { /* need to explicitly avoid our custom _private field cleanup when called from internal XSLT cleanup (xsltApplyStylesheetInternal -> xsltFreeTransformContext -> xsltFreeRVTs -> xmlFreeDoc) onto result tree fragments, represented as standalone documents with otherwise infeasible space-prefixed name (xsltInternals.h: XSLT_MARK_RES_TREE_FRAG) and carrying it's own load at _private field -- later assert on the XML_PRIVATE_MAGIC would explode */ if (node->type != XML_DOCUMENT_NODE || node->name == NULL || node->name[0] != ' ') { __xml_private_free(node->_private); } } static void pcmkRegisterNode(xmlNodePtr node) { xml_private_t *p = NULL; switch(node->type) { case XML_ELEMENT_NODE: case XML_DOCUMENT_NODE: case XML_ATTRIBUTE_NODE: case XML_COMMENT_NODE: p = calloc(1, sizeof(xml_private_t)); p->check = XML_PRIVATE_MAGIC; /* Flags will be reset if necessary when tracking is enabled */ p->flags |= (xpf_dirty|xpf_created); node->_private = p; break; case XML_TEXT_NODE: case XML_DTD_NODE: case XML_CDATA_SECTION_NODE: break; default: /* Ignore */ crm_trace("Ignoring %p %d", node, node->type); CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE); break; } if(p && pcmk__tracking_xml_changes(node, FALSE)) { /* XML_ELEMENT_NODE doesn't get picked up here, node->doc is * not hooked up at the point we are called */ pcmk__set_xml_flag(node, xpf_dirty); __xml_node_dirty(node); } } void xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls) { xml_accept_changes(xml); crm_trace("Tracking changes%s to %p", enforce_acls?" with ACLs":"", xml); pcmk__set_xml_flag(xml, xpf_tracking); if(enforce_acls) { if(acl_source == NULL) { acl_source = xml; } pcmk__set_xml_flag(xml, xpf_acl_enabled); pcmk__unpack_acl(acl_source, xml, user); pcmk__apply_acl(xml); } } bool xml_tracking_changes(xmlNode * xml) { if(xml == NULL) { return FALSE; } else if(is_set(((xml_private_t *)xml->doc->_private)->flags, xpf_tracking)) { return TRUE; } return FALSE; } bool xml_document_dirty(xmlNode *xml) { if(xml != NULL && xml->doc && xml->doc->_private) { xml_private_t *doc = xml->doc->_private; return is_set(doc->flags, xpf_dirty); } return FALSE; } /* */ static int __xml_offset(xmlNode *xml) { int position = 0; xmlNode *cIter = NULL; for(cIter = xml; cIter->prev; cIter = cIter->prev) { xml_private_t *p = ((xmlNode*)cIter->prev)->_private; if(is_not_set(p->flags, xpf_skip)) { position++; } } return position; } static int __xml_offset_no_deletions(xmlNode *xml) { int position = 0; xmlNode *cIter = NULL; for(cIter = xml; cIter->prev; cIter = cIter->prev) { xml_private_t *p = ((xmlNode*)cIter->prev)->_private; if(is_not_set(p->flags, xpf_deleted)) { position++; } } return position; } static void __xml_build_changes(xmlNode * xml, xmlNode *patchset) { xmlNode *cIter = NULL; xmlAttr *pIter = NULL; xmlNode *change = NULL; xml_private_t *p = xml->_private; if(patchset && is_set(p->flags, xpf_created)) { int offset = 0; char buffer[XML_BUFFER_SIZE]; if (pcmk__element_xpath(NULL, xml->parent, buffer, offset, sizeof(buffer)) > 0) { int position = __xml_offset_no_deletions(xml); change = create_xml_node(patchset, XML_DIFF_CHANGE); crm_xml_add(change, XML_DIFF_OP, "create"); crm_xml_add(change, XML_DIFF_PATH, buffer); crm_xml_add_int(change, XML_DIFF_POSITION, position); add_node_copy(change, xml); } return; } for (pIter = pcmk__first_xml_attr(xml); pIter != NULL; pIter = pIter->next) { xmlNode *attr = NULL; p = pIter->_private; if(is_not_set(p->flags, xpf_deleted) && is_not_set(p->flags, xpf_dirty)) { continue; } if(change == NULL) { int offset = 0; char buffer[XML_BUFFER_SIZE]; if (pcmk__element_xpath(NULL, xml, buffer, offset, sizeof(buffer)) > 0) { change = create_xml_node(patchset, XML_DIFF_CHANGE); crm_xml_add(change, XML_DIFF_OP, "modify"); crm_xml_add(change, XML_DIFF_PATH, buffer); change = create_xml_node(change, XML_DIFF_LIST); } } attr = create_xml_node(change, XML_DIFF_ATTR); crm_xml_add(attr, XML_NVPAIR_ATTR_NAME, (const char *)pIter->name); if(p->flags & xpf_deleted) { crm_xml_add(attr, XML_DIFF_OP, "unset"); } else { const char *value = crm_element_value(xml, (const char *)pIter->name); crm_xml_add(attr, XML_DIFF_OP, "set"); crm_xml_add(attr, XML_NVPAIR_ATTR_VALUE, value); } } if(change) { xmlNode *result = NULL; change = create_xml_node(change->parent, XML_DIFF_RESULT); result = create_xml_node(change, (const char *)xml->name); for (pIter = pcmk__first_xml_attr(xml); pIter != NULL; pIter = pIter->next) { const char *value = crm_element_value(xml, (const char *)pIter->name); p = pIter->_private; if (is_not_set(p->flags, xpf_deleted)) { crm_xml_add(result, (const char *)pIter->name, value); } } } for (cIter = __xml_first_child(xml); cIter != NULL; cIter = __xml_next(cIter)) { __xml_build_changes(cIter, patchset); } p = xml->_private; if(patchset && is_set(p->flags, xpf_moved)) { int offset = 0; char buffer[XML_BUFFER_SIZE]; crm_trace("%s.%s moved to position %d", xml->name, ID(xml), __xml_offset(xml)); if (pcmk__element_xpath(NULL, xml, buffer, offset, sizeof(buffer)) > 0) { change = create_xml_node(patchset, XML_DIFF_CHANGE); crm_xml_add(change, XML_DIFF_OP, "move"); crm_xml_add(change, XML_DIFF_PATH, buffer); crm_xml_add_int(change, XML_DIFF_POSITION, __xml_offset_no_deletions(xml)); } } } static void __xml_accept_changes(xmlNode * xml) { xmlNode *cIter = NULL; xmlAttr *pIter = NULL; xml_private_t *p = xml->_private; p->flags = xpf_none; pIter = pcmk__first_xml_attr(xml); while (pIter != NULL) { const xmlChar *name = pIter->name; p = pIter->_private; pIter = pIter->next; if(p->flags & xpf_deleted) { xml_remove_prop(xml, (const char *)name); } else { p->flags = xpf_none; } } for (cIter = __xml_first_child(xml); cIter != NULL; cIter = __xml_next(cIter)) { __xml_accept_changes(cIter); } } static bool is_config_change(xmlNode *xml) { GListPtr gIter = NULL; xml_private_t *p = NULL; xmlNode *config = first_named_child(xml, XML_CIB_TAG_CONFIGURATION); if(config) { p = config->_private; } if(p && is_set(p->flags, xpf_dirty)) { return TRUE; } if(xml->doc && xml->doc->_private) { p = xml->doc->_private; for(gIter = p->deleted_objs; gIter; gIter = gIter->next) { xml_deleted_obj_t *deleted_obj = gIter->data; if(strstr(deleted_obj->path, "/"XML_TAG_CIB"/"XML_CIB_TAG_CONFIGURATION) != NULL) { return TRUE; } } } return FALSE; } static void xml_repair_v1_diff(xmlNode * last, xmlNode * next, xmlNode * local_diff, gboolean changed) { int lpc = 0; xmlNode *cib = NULL; xmlNode *diff_child = NULL; const char *tag = NULL; const char *vfields[] = { XML_ATTR_GENERATION_ADMIN, XML_ATTR_GENERATION, XML_ATTR_NUMUPDATES, }; if (local_diff == NULL) { crm_trace("Nothing to do"); return; } tag = "diff-removed"; diff_child = find_xml_node(local_diff, tag, FALSE); if (diff_child == NULL) { diff_child = create_xml_node(local_diff, tag); } tag = XML_TAG_CIB; cib = find_xml_node(diff_child, tag, FALSE); if (cib == NULL) { cib = create_xml_node(diff_child, tag); } for(lpc = 0; last && lpc < DIMOF(vfields); lpc++){ const char *value = crm_element_value(last, vfields[lpc]); crm_xml_add(diff_child, vfields[lpc], value); if(changed || lpc == 2) { crm_xml_add(cib, vfields[lpc], value); } } tag = "diff-added"; diff_child = find_xml_node(local_diff, tag, FALSE); if (diff_child == NULL) { diff_child = create_xml_node(local_diff, tag); } tag = XML_TAG_CIB; cib = find_xml_node(diff_child, tag, FALSE); if (cib == NULL) { cib = create_xml_node(diff_child, tag); } for(lpc = 0; next && lpc < DIMOF(vfields); lpc++){ const char *value = crm_element_value(next, vfields[lpc]); crm_xml_add(diff_child, vfields[lpc], value); } if (next) { xmlAttrPtr xIter = NULL; for (xIter = next->properties; xIter; xIter = xIter->next) { const char *p_name = (const char *)xIter->name; const char *p_value = crm_element_value(next, p_name); xmlSetProp(cib, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value); } } crm_log_xml_explicit(local_diff, "Repaired-diff"); } static xmlNode * xml_create_patchset_v1(xmlNode *source, xmlNode *target, bool config, bool suppress) { xmlNode *patchset = diff_xml_object(source, target, suppress); if(patchset) { CRM_LOG_ASSERT(xml_document_dirty(target)); xml_repair_v1_diff(source, target, patchset, config); crm_xml_add(patchset, "format", "1"); } return patchset; } static xmlNode * xml_create_patchset_v2(xmlNode *source, xmlNode *target) { int lpc = 0; GListPtr gIter = NULL; xml_private_t *doc = NULL; xmlNode *v = NULL; xmlNode *version = NULL; xmlNode *patchset = NULL; const char *vfields[] = { XML_ATTR_GENERATION_ADMIN, XML_ATTR_GENERATION, XML_ATTR_NUMUPDATES, }; CRM_ASSERT(target); if(xml_document_dirty(target) == FALSE) { return NULL; } CRM_ASSERT(target->doc); doc = target->doc->_private; patchset = create_xml_node(NULL, XML_TAG_DIFF); crm_xml_add_int(patchset, "format", 2); version = create_xml_node(patchset, XML_DIFF_VERSION); v = create_xml_node(version, XML_DIFF_VSOURCE); for(lpc = 0; lpc < DIMOF(vfields); lpc++){ const char *value = crm_element_value(source, vfields[lpc]); if(value == NULL) { value = "1"; } crm_xml_add(v, vfields[lpc], value); } v = create_xml_node(version, XML_DIFF_VTARGET); for(lpc = 0; lpc < DIMOF(vfields); lpc++){ const char *value = crm_element_value(target, vfields[lpc]); if(value == NULL) { value = "1"; } crm_xml_add(v, vfields[lpc], value); } for(gIter = doc->deleted_objs; gIter; gIter = gIter->next) { xml_deleted_obj_t *deleted_obj = gIter->data; xmlNode *change = create_xml_node(patchset, XML_DIFF_CHANGE); crm_xml_add(change, XML_DIFF_OP, "delete"); crm_xml_add(change, XML_DIFF_PATH, deleted_obj->path); if (deleted_obj->position >= 0) { crm_xml_add_int(change, XML_DIFF_POSITION, deleted_obj->position); } } __xml_build_changes(target, patchset); return patchset; } xmlNode * xml_create_patchset(int format, xmlNode *source, xmlNode *target, bool *config_changed, bool manage_version) { int counter = 0; bool config = FALSE; xmlNode *patch = NULL; const char *version = crm_element_value(source, XML_ATTR_CRM_VERSION); xml_acl_disable(target); if(xml_document_dirty(target) == FALSE) { crm_trace("No change %d", format); return NULL; /* No change */ } config = is_config_change(target); if(config_changed) { *config_changed = config; } if(manage_version && config) { crm_trace("Config changed %d", format); crm_xml_add(target, XML_ATTR_NUMUPDATES, "0"); crm_element_value_int(target, XML_ATTR_GENERATION, &counter); crm_xml_add_int(target, XML_ATTR_GENERATION, counter+1); } else if(manage_version) { crm_element_value_int(target, XML_ATTR_NUMUPDATES, &counter); crm_trace("Status changed %d - %d %s", format, counter, crm_element_value(source, XML_ATTR_NUMUPDATES)); crm_xml_add_int(target, XML_ATTR_NUMUPDATES, counter+1); } if(format == 0) { if (compare_version("3.0.8", version) < 0) { format = 2; } else { format = 1; } crm_trace("Using patch format %d for version: %s", format, version); } switch(format) { case 1: patch = xml_create_patchset_v1(source, target, config, FALSE); break; case 2: patch = xml_create_patchset_v2(source, target); break; default: crm_err("Unknown patch format: %d", format); return NULL; } return patch; } void patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target, bool with_digest) { int format = 1; const char *version = NULL; char *digest = NULL; if (patch == NULL || source == NULL || target == NULL) { return; } /* NOTE: We should always call xml_accept_changes() before calculating digest. */ /* Otherwise, with an on-tracking dirty target, we could get a wrong digest. */ CRM_LOG_ASSERT(xml_document_dirty(target) == FALSE); crm_element_value_int(patch, "format", &format); if (format > 1 && with_digest == FALSE) { return; } version = crm_element_value(source, XML_ATTR_CRM_VERSION); digest = calculate_xml_versioned_digest(target, FALSE, TRUE, version); crm_xml_add(patch, XML_ATTR_DIGEST, digest); free(digest); return; } static void __xml_log_element(int log_level, const char *file, const char *function, int line, const char *prefix, xmlNode * data, int depth, int options); void xml_log_patchset(uint8_t log_level, const char *function, xmlNode * patchset) { int format = 1; xmlNode *child = NULL; xmlNode *added = NULL; xmlNode *removed = NULL; gboolean is_first = TRUE; int add[] = { 0, 0, 0 }; int del[] = { 0, 0, 0 }; const char *fmt = NULL; const char *digest = NULL; int options = xml_log_option_formatted; static struct qb_log_callsite *patchset_cs = NULL; if (patchset_cs == NULL) { patchset_cs = qb_log_callsite_get(function, __FILE__, "xml-patchset", log_level, __LINE__, 0); } if (patchset == NULL) { crm_trace("Empty patch"); return; } else if (log_level == 0) { /* Log to stdout */ } else if (crm_is_callsite_active(patchset_cs, log_level, 0) == FALSE) { return; } xml_patch_versions(patchset, add, del); fmt = crm_element_value(patchset, "format"); digest = crm_element_value(patchset, XML_ATTR_DIGEST); if (add[2] != del[2] || add[1] != del[1] || add[0] != del[0]) { do_crm_log_alias(log_level, __FILE__, function, __LINE__, "Diff: --- %d.%d.%d %s", del[0], del[1], del[2], fmt); do_crm_log_alias(log_level, __FILE__, function, __LINE__, "Diff: +++ %d.%d.%d %s", add[0], add[1], add[2], digest); } else if (patchset != NULL && (add[0] || add[1] || add[2])) { do_crm_log_alias(log_level, __FILE__, function, __LINE__, "%s: Local-only Change: %d.%d.%d", function ? function : "", add[0], add[1], add[2]); } crm_element_value_int(patchset, "format", &format); if(format == 2) { xmlNode *change = NULL; for (change = __xml_first_child(patchset); change != NULL; change = __xml_next(change)) { const char *op = crm_element_value(change, XML_DIFF_OP); const char *xpath = crm_element_value(change, XML_DIFF_PATH); if(op == NULL) { } else if(strcmp(op, "create") == 0) { int lpc = 0, max = 0; char *prefix = crm_strdup_printf("++ %s: ", xpath); max = strlen(prefix); __xml_log_element(log_level, __FILE__, function, __LINE__, prefix, change->children, 0, xml_log_option_formatted|xml_log_option_open); for(lpc = 2; lpc < max; lpc++) { prefix[lpc] = ' '; } __xml_log_element(log_level, __FILE__, function, __LINE__, prefix, change->children, 0, xml_log_option_formatted|xml_log_option_close|xml_log_option_children); free(prefix); } else if(strcmp(op, "move") == 0) { do_crm_log_alias(log_level, __FILE__, function, __LINE__, "+~ %s moved to offset %s", xpath, crm_element_value(change, XML_DIFF_POSITION)); } else if(strcmp(op, "modify") == 0) { xmlNode *clist = first_named_child(change, XML_DIFF_LIST); char buffer_set[XML_BUFFER_SIZE]; char buffer_unset[XML_BUFFER_SIZE]; int o_set = 0; int o_unset = 0; buffer_set[0] = 0; buffer_unset[0] = 0; for (child = __xml_first_child(clist); child != NULL; child = __xml_next(child)) { const char *name = crm_element_value(child, "name"); op = crm_element_value(child, XML_DIFF_OP); if(op == NULL) { } else if(strcmp(op, "set") == 0) { const char *value = crm_element_value(child, "value"); if(o_set > 0) { o_set += snprintf(buffer_set + o_set, XML_BUFFER_SIZE - o_set, ", "); } o_set += snprintf(buffer_set + o_set, XML_BUFFER_SIZE - o_set, "@%s=%s", name, value); } else if(strcmp(op, "unset") == 0) { if(o_unset > 0) { o_unset += snprintf(buffer_unset + o_unset, XML_BUFFER_SIZE - o_unset, ", "); } o_unset += snprintf(buffer_unset + o_unset, XML_BUFFER_SIZE - o_unset, "@%s", name); } } if(o_set) { do_crm_log_alias(log_level, __FILE__, function, __LINE__, "+ %s: %s", xpath, buffer_set); } if(o_unset) { do_crm_log_alias(log_level, __FILE__, function, __LINE__, "-- %s: %s", xpath, buffer_unset); } } else if(strcmp(op, "delete") == 0) { int position = -1; crm_element_value_int(change, XML_DIFF_POSITION, &position); if (position >= 0) { do_crm_log_alias(log_level, __FILE__, function, __LINE__, "-- %s (%d)", xpath, position); } else { do_crm_log_alias(log_level, __FILE__, function, __LINE__, "-- %s", xpath); } } } return; } if (log_level < LOG_DEBUG || function == NULL) { options |= xml_log_option_diff_short; } removed = find_xml_node(patchset, "diff-removed", FALSE); for (child = __xml_first_child(removed); child != NULL; child = __xml_next(child)) { log_data_element(log_level, __FILE__, function, __LINE__, "- ", child, 0, options | xml_log_option_diff_minus); if (is_first) { is_first = FALSE; } else { do_crm_log_alias(log_level, __FILE__, function, __LINE__, " --- "); } } is_first = TRUE; added = find_xml_node(patchset, "diff-added", FALSE); for (child = __xml_first_child(added); child != NULL; child = __xml_next(child)) { log_data_element(log_level, __FILE__, function, __LINE__, "+ ", child, 0, options | xml_log_option_diff_plus); if (is_first) { is_first = FALSE; } else { do_crm_log_alias(log_level, __FILE__, function, __LINE__, " +++ "); } } } void xml_log_changes(uint8_t log_level, const char *function, xmlNode * xml) { GListPtr gIter = NULL; xml_private_t *doc = NULL; CRM_ASSERT(xml); CRM_ASSERT(xml->doc); doc = xml->doc->_private; if(is_not_set(doc->flags, xpf_dirty)) { return; } for(gIter = doc->deleted_objs; gIter; gIter = gIter->next) { xml_deleted_obj_t *deleted_obj = gIter->data; if (deleted_obj->position >= 0) { do_crm_log_alias(log_level, __FILE__, function, __LINE__, "-- %s (%d)", deleted_obj->path, deleted_obj->position); } else { do_crm_log_alias(log_level, __FILE__, function, __LINE__, "-- %s", deleted_obj->path); } } log_data_element(log_level, __FILE__, function, __LINE__, "+ ", xml, 0, xml_log_option_formatted|xml_log_option_dirty_add); } void xml_accept_changes(xmlNode * xml) { xmlNode *top = NULL; xml_private_t *doc = NULL; if(xml == NULL) { return; } crm_trace("Accepting changes to %p", xml); doc = xml->doc->_private; top = xmlDocGetRootElement(xml->doc); __xml_private_clean(xml->doc->_private); if(is_not_set(doc->flags, xpf_dirty)) { doc->flags = xpf_none; return; } doc->flags = xpf_none; __xml_accept_changes(top); } static xmlNode * find_element(xmlNode *haystack, xmlNode *needle, gboolean exact) { CRM_CHECK(needle != NULL, return NULL); return (needle->type == XML_COMMENT_NODE)? find_xml_comment(haystack, needle, exact) : find_entity(haystack, crm_element_name(needle), ID(needle)); } /* Simplified version for applying v1-style XML patches */ static void __subtract_xml_object(xmlNode * target, xmlNode * patch) { xmlNode *patch_child = NULL; xmlNode *cIter = NULL; xmlAttrPtr xIter = NULL; char *id = NULL; const char *name = NULL; const char *value = NULL; if (target == NULL || patch == NULL) { return; } if (target->type == XML_COMMENT_NODE) { gboolean dummy; subtract_xml_comment(target->parent, target, patch, &dummy); } name = crm_element_name(target); CRM_CHECK(name != NULL, return); CRM_CHECK(safe_str_eq(crm_element_name(target), crm_element_name(patch)), return); CRM_CHECK(safe_str_eq(ID(target), ID(patch)), return); /* check for XML_DIFF_MARKER in a child */ id = crm_element_value_copy(target, XML_ATTR_ID); value = crm_element_value(patch, XML_DIFF_MARKER); if (value != NULL && strcmp(value, "removed:top") == 0) { crm_trace("We are the root of the deletion: %s.id=%s", name, id); free_xml(target); free(id); return; } for (xIter = pcmk__first_xml_attr(patch); xIter != NULL; xIter = xIter->next) { const char *p_name = (const char *)xIter->name; /* Removing and then restoring the id field would change the ordering of properties */ if (safe_str_neq(p_name, XML_ATTR_ID)) { xml_remove_prop(target, p_name); } } /* changes to child objects */ cIter = __xml_first_child(target); while (cIter) { xmlNode *target_child = cIter; cIter = __xml_next(cIter); patch_child = find_element(patch, target_child, FALSE); __subtract_xml_object(target_child, patch_child); } free(id); } static void __add_xml_object(xmlNode * parent, xmlNode * target, xmlNode * patch) { xmlNode *patch_child = NULL; xmlNode *target_child = NULL; xmlAttrPtr xIter = NULL; const char *id = NULL; const char *name = NULL; const char *value = NULL; if (patch == NULL) { return; } else if (parent == NULL && target == NULL) { return; } /* check for XML_DIFF_MARKER in a child */ value = crm_element_value(patch, XML_DIFF_MARKER); if (target == NULL && value != NULL && strcmp(value, "added:top") == 0) { id = ID(patch); name = crm_element_name(patch); crm_trace("We are the root of the addition: %s.id=%s", name, id); add_node_copy(parent, patch); return; } else if(target == NULL) { id = ID(patch); name = crm_element_name(patch); crm_err("Could not locate: %s.id=%s", name, id); return; } if (target->type == XML_COMMENT_NODE) { add_xml_comment(parent, target, patch); } name = crm_element_name(target); CRM_CHECK(name != NULL, return); CRM_CHECK(safe_str_eq(crm_element_name(target), crm_element_name(patch)), return); CRM_CHECK(safe_str_eq(ID(target), ID(patch)), return); for (xIter = pcmk__first_xml_attr(patch); xIter != NULL; xIter = xIter->next) { const char *p_name = (const char *)xIter->name; const char *p_value = crm_element_value(patch, p_name); xml_remove_prop(target, p_name); /* Preserve the patch order */ crm_xml_add(target, p_name, p_value); } /* changes to child objects */ for (patch_child = __xml_first_child(patch); patch_child != NULL; patch_child = __xml_next(patch_child)) { target_child = find_element(target, patch_child, FALSE); __add_xml_object(target, target_child, patch_child); } } /*! * \internal * \brief Find additions or removals in a patch set * * \param[in] patchset XML of patch * \param[in] format Patch version * \param[in] added TRUE if looking for additions, FALSE if removals * \param[in,out] patch_node Will be set to node if found * * \return TRUE if format is valid, FALSE if invalid */ static bool find_patch_xml_node(xmlNode *patchset, int format, bool added, xmlNode **patch_node) { xmlNode *cib_node; const char *label; switch(format) { case 1: label = added? "diff-added" : "diff-removed"; *patch_node = find_xml_node(patchset, label, FALSE); cib_node = find_xml_node(*patch_node, "cib", FALSE); if (cib_node != NULL) { *patch_node = cib_node; } break; case 2: label = added? "target" : "source"; *patch_node = find_xml_node(patchset, "version", FALSE); *patch_node = find_xml_node(*patch_node, label, FALSE); break; default: crm_warn("Unknown patch format: %d", format); *patch_node = NULL; return FALSE; } return TRUE; } bool xml_patch_versions(xmlNode *patchset, int add[3], int del[3]) { int lpc = 0; int format = 1; xmlNode *tmp = NULL; const char *vfields[] = { XML_ATTR_GENERATION_ADMIN, XML_ATTR_GENERATION, XML_ATTR_NUMUPDATES, }; crm_element_value_int(patchset, "format", &format); /* Process removals */ if (!find_patch_xml_node(patchset, format, FALSE, &tmp)) { return -EINVAL; } if (tmp) { for(lpc = 0; lpc < DIMOF(vfields); lpc++) { crm_element_value_int(tmp, vfields[lpc], &(del[lpc])); crm_trace("Got %d for del[%s]", del[lpc], vfields[lpc]); } } /* Process additions */ if (!find_patch_xml_node(patchset, format, TRUE, &tmp)) { return -EINVAL; } if (tmp) { for(lpc = 0; lpc < DIMOF(vfields); lpc++) { crm_element_value_int(tmp, vfields[lpc], &(add[lpc])); crm_trace("Got %d for add[%s]", add[lpc], vfields[lpc]); } } return pcmk_ok; } static int xml_patch_version_check(xmlNode *xml, xmlNode *patchset, int format) { int lpc = 0; bool changed = FALSE; int this[] = { 0, 0, 0 }; int add[] = { 0, 0, 0 }; int del[] = { 0, 0, 0 }; const char *vfields[] = { XML_ATTR_GENERATION_ADMIN, XML_ATTR_GENERATION, XML_ATTR_NUMUPDATES, }; for(lpc = 0; lpc < DIMOF(vfields); lpc++) { crm_element_value_int(xml, vfields[lpc], &(this[lpc])); crm_trace("Got %d for this[%s]", this[lpc], vfields[lpc]); if (this[lpc] < 0) { this[lpc] = 0; } } /* Set some defaults in case nothing is present */ add[0] = this[0]; add[1] = this[1]; add[2] = this[2] + 1; for(lpc = 0; lpc < DIMOF(vfields); lpc++) { del[lpc] = this[lpc]; } xml_patch_versions(patchset, add, del); for(lpc = 0; lpc < DIMOF(vfields); lpc++) { if(this[lpc] < del[lpc]) { crm_debug("Current %s is too low (%d.%d.%d < %d.%d.%d --> %d.%d.%d)", vfields[lpc], this[0], this[1], this[2], del[0], del[1], del[2], add[0], add[1], add[2]); return -pcmk_err_diff_resync; } else if(this[lpc] > del[lpc]) { crm_info("Current %s is too high (%d.%d.%d > %d.%d.%d --> %d.%d.%d) %p", vfields[lpc], this[0], this[1], this[2], del[0], del[1], del[2], add[0], add[1], add[2], patchset); crm_log_xml_info(patchset, "OldPatch"); return -pcmk_err_old_data; } } for(lpc = 0; lpc < DIMOF(vfields); lpc++) { if(add[lpc] > del[lpc]) { changed = TRUE; } } if(changed == FALSE) { crm_notice("Versions did not change in patch %d.%d.%d", add[0], add[1], add[2]); return -pcmk_err_old_data; } crm_debug("Can apply patch %d.%d.%d to %d.%d.%d", add[0], add[1], add[2], this[0], this[1], this[2]); return pcmk_ok; } static int xml_apply_patchset_v1(xmlNode *xml, xmlNode *patchset) { int rc = pcmk_ok; int root_nodes_seen = 0; xmlNode *child_diff = NULL; xmlNode *added = find_xml_node(patchset, "diff-added", FALSE); xmlNode *removed = find_xml_node(patchset, "diff-removed", FALSE); xmlNode *old = copy_xml(xml); crm_trace("Subtraction Phase"); for (child_diff = __xml_first_child(removed); child_diff != NULL; child_diff = __xml_next(child_diff)) { CRM_CHECK(root_nodes_seen == 0, rc = FALSE); if (root_nodes_seen == 0) { __subtract_xml_object(xml, child_diff); } root_nodes_seen++; } if (root_nodes_seen > 1) { crm_err("(-) Diffs cannot contain more than one change set... saw %d", root_nodes_seen); rc = -ENOTUNIQ; } root_nodes_seen = 0; crm_trace("Addition Phase"); if (rc == pcmk_ok) { xmlNode *child_diff = NULL; for (child_diff = __xml_first_child(added); child_diff != NULL; child_diff = __xml_next(child_diff)) { CRM_CHECK(root_nodes_seen == 0, rc = FALSE); if (root_nodes_seen == 0) { __add_xml_object(NULL, xml, child_diff); } root_nodes_seen++; } } if (root_nodes_seen > 1) { crm_err("(+) Diffs cannot contain more than one change set... saw %d", root_nodes_seen); rc = -ENOTUNIQ; } purge_diff_markers(xml); /* Purge prior to checking the digest */ free_xml(old); return rc; } static xmlNode * __first_xml_child_match(xmlNode *parent, const char *name, const char *id, int position) { xmlNode *cIter = NULL; for (cIter = __xml_first_child(parent); cIter != NULL; cIter = __xml_next(cIter)) { if(strcmp((const char *)cIter->name, name) != 0) { continue; } else if(id) { const char *cid = ID(cIter); if(cid == NULL || strcmp(cid, id) != 0) { continue; } } /* The "position" makes sense only for XML comments for now */ if (cIter->type == XML_COMMENT_NODE && position >= 0 && __xml_offset(cIter) != position) { continue; } return cIter; } return NULL; } /*! * \internal * \brief Simplified, more efficient alternative to get_xpath_object() * * \param[in] top Root of XML to search * \param[in] key Search xpath * \param[in] target_position If deleting, where to delete * * \return XML child matching xpath if found, NULL otherwise * * \note This only works on simplified xpaths found in v2 patchset diffs, * i.e. the only allowed search predicate is [@id='XXX']. */ static xmlNode * __xml_find_path(xmlNode *top, const char *key, int target_position) { xmlNode *target = (xmlNode*) top->doc; const char *current = key; char *section; char *remainder; char *id; char *tag; char *path = NULL; int rc; size_t key_len; CRM_CHECK(key != NULL, return NULL); key_len = strlen(key); /* These are scanned from key after a slash, so they can't be bigger * than key_len - 1 characters plus a null terminator. */ remainder = calloc(key_len, sizeof(char)); CRM_ASSERT(remainder != NULL); section = calloc(key_len, sizeof(char)); CRM_ASSERT(section != NULL); id = calloc(key_len, sizeof(char)); CRM_ASSERT(id != NULL); tag = calloc(key_len, sizeof(char)); CRM_ASSERT(tag != NULL); do { // Look for /NEXT_COMPONENT/REMAINING_COMPONENTS rc = sscanf(current, "/%[^/]%s", section, remainder); if (rc > 0) { // Separate FIRST_COMPONENT into TAG[@id='ID'] int f = sscanf(section, "%[^[][@id='%[^']", tag, id); int current_position = -1; /* The target position is for the final component tag, so only use * it if there is nothing left to search after this component. */ if ((rc == 1) && (target_position >= 0)) { current_position = target_position; } switch (f) { case 1: target = __first_xml_child_match(target, tag, NULL, current_position); break; case 2: target = __first_xml_child_match(target, tag, id, current_position); break; default: // This should not be possible target = NULL; break; } current = remainder; } // Continue if something remains to search, and we've matched so far } while ((rc == 2) && target); if (target) { crm_trace("Found %s for %s", (path = (char *) xmlGetNodePath(target)), key); free(path); } else { crm_debug("No match for %s", key); } free(remainder); free(section); free(tag); free(id); return target; } typedef struct xml_change_obj_s { xmlNode *change; xmlNode *match; } xml_change_obj_t; static gint sort_change_obj_by_position(gconstpointer a, gconstpointer b) { const xml_change_obj_t *change_obj_a = a; const xml_change_obj_t *change_obj_b = b; int position_a = -1; int position_b = -1; crm_element_value_int(change_obj_a->change, XML_DIFF_POSITION, &position_a); crm_element_value_int(change_obj_b->change, XML_DIFF_POSITION, &position_b); if (position_a < position_b) { return -1; } else if (position_a > position_b) { return 1; } return 0; } static int xml_apply_patchset_v2(xmlNode *xml, xmlNode *patchset) { int rc = pcmk_ok; xmlNode *change = NULL; GListPtr change_objs = NULL; GListPtr gIter = NULL; for (change = __xml_first_child(patchset); change != NULL; change = __xml_next(change)) { xmlNode *match = NULL; const char *op = crm_element_value(change, XML_DIFF_OP); const char *xpath = crm_element_value(change, XML_DIFF_PATH); int position = -1; if(op == NULL) { continue; } crm_trace("Processing %s %s", change->name, op); // "delete" changes for XML comments are generated with "position" if(strcmp(op, "delete") == 0) { crm_element_value_int(change, XML_DIFF_POSITION, &position); } match = __xml_find_path(xml, xpath, position); crm_trace("Performing %s on %s with %p", op, xpath, match); if(match == NULL && strcmp(op, "delete") == 0) { crm_debug("No %s match for %s in %p", op, xpath, xml->doc); continue; } else if(match == NULL) { crm_err("No %s match for %s in %p", op, xpath, xml->doc); rc = -pcmk_err_diff_failed; continue; } else if (strcmp(op, "create") == 0 || strcmp(op, "move") == 0) { // Delay the adding of a "create" object xml_change_obj_t *change_obj = calloc(1, sizeof(xml_change_obj_t)); CRM_ASSERT(change_obj != NULL); change_obj->change = change; change_obj->match = match; change_objs = g_list_append(change_objs, change_obj); if (strcmp(op, "move") == 0) { // Temporarily put the "move" object after the last sibling if (match->parent != NULL && match->parent->last != NULL) { xmlAddNextSibling(match->parent->last, match); } } } else if(strcmp(op, "delete") == 0) { free_xml(match); } else if(strcmp(op, "modify") == 0) { xmlAttr *pIter = pcmk__first_xml_attr(match); xmlNode *attrs = __xml_first_child(first_named_child(change, XML_DIFF_RESULT)); if(attrs == NULL) { rc = -ENOMSG; continue; } while(pIter != NULL) { const char *name = (const char *)pIter->name; pIter = pIter->next; xml_remove_prop(match, name); } for (pIter = pcmk__first_xml_attr(attrs); pIter != NULL; pIter = pIter->next) { const char *name = (const char *)pIter->name; const char *value = crm_element_value(attrs, name); crm_xml_add(match, name, value); } } else { crm_err("Unknown operation: %s", op); rc = -pcmk_err_diff_failed; } } // Changes should be generated in the right order. Double checking. change_objs = g_list_sort(change_objs, sort_change_obj_by_position); for (gIter = change_objs; gIter; gIter = gIter->next) { xml_change_obj_t *change_obj = gIter->data; xmlNode *match = change_obj->match; const char *op = NULL; const char *xpath = NULL; change = change_obj->change; op = crm_element_value(change, XML_DIFF_OP); xpath = crm_element_value(change, XML_DIFF_PATH); crm_trace("Continue performing %s on %s with %p", op, xpath, match); if(strcmp(op, "create") == 0) { int position = 0; xmlNode *child = NULL; xmlNode *match_child = NULL; match_child = match->children; crm_element_value_int(change, XML_DIFF_POSITION, &position); while(match_child && position != __xml_offset(match_child)) { match_child = match_child->next; } child = xmlDocCopyNode(change->children, match->doc, 1); if(match_child) { crm_trace("Adding %s at position %d", child->name, position); xmlAddPrevSibling(match_child, child); } else if(match->last) { /* Add to the end */ crm_trace("Adding %s at position %d (end)", child->name, position); xmlAddNextSibling(match->last, child); } else { crm_trace("Adding %s at position %d (first)", child->name, position); CRM_LOG_ASSERT(position == 0); xmlAddChild(match, child); } crm_node_created(child); } else if(strcmp(op, "move") == 0) { int position = 0; crm_element_value_int(change, XML_DIFF_POSITION, &position); if(position != __xml_offset(match)) { xmlNode *match_child = NULL; int p = position; if(p > __xml_offset(match)) { p++; /* Skip ourselves */ } CRM_ASSERT(match->parent != NULL); match_child = match->parent->children; while(match_child && p != __xml_offset(match_child)) { match_child = match_child->next; } crm_trace("Moving %s to position %d (was %d, prev %p, %s %p)", match->name, position, __xml_offset(match), match->prev, match_child?"next":"last", match_child?match_child:match->parent->last); if(match_child) { xmlAddPrevSibling(match_child, match); } else { CRM_ASSERT(match->parent->last != NULL); xmlAddNextSibling(match->parent->last, match); } } else { crm_trace("%s is already in position %d", match->name, position); } if(position != __xml_offset(match)) { crm_err("Moved %s.%s to position %d instead of %d (%p)", match->name, ID(match), __xml_offset(match), position, match->prev); rc = -pcmk_err_diff_failed; } } } g_list_free_full(change_objs, free); return rc; } int xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version) { int format = 1; int rc = pcmk_ok; xmlNode *old = NULL; const char *digest = crm_element_value(patchset, XML_ATTR_DIGEST); if(patchset == NULL) { return rc; } xml_log_patchset(LOG_TRACE, __FUNCTION__, patchset); crm_element_value_int(patchset, "format", &format); if(check_version) { rc = xml_patch_version_check(xml, patchset, format); if(rc != pcmk_ok) { return rc; } } if(digest) { /* Make it available for logging if the result doesn't have the expected digest */ old = copy_xml(xml); } if(rc == pcmk_ok) { switch(format) { case 1: rc = xml_apply_patchset_v1(xml, patchset); break; case 2: rc = xml_apply_patchset_v2(xml, patchset); break; default: crm_err("Unknown patch format: %d", format); rc = -EINVAL; } } if(rc == pcmk_ok && digest) { static struct qb_log_callsite *digest_cs = NULL; char *new_digest = NULL; char *version = crm_element_value_copy(xml, XML_ATTR_CRM_VERSION); if (digest_cs == NULL) { digest_cs = qb_log_callsite_get(__func__, __FILE__, "diff-digest", LOG_TRACE, __LINE__, crm_trace_nonlog); } new_digest = calculate_xml_versioned_digest(xml, FALSE, TRUE, version); if (safe_str_neq(new_digest, digest)) { crm_info("v%d digest mis-match: expected %s, calculated %s", format, digest, new_digest); rc = -pcmk_err_diff_failed; if (digest_cs && digest_cs->targets) { save_xml_to_file(old, "PatchDigest:input", NULL); save_xml_to_file(xml, "PatchDigest:result", NULL); save_xml_to_file(patchset,"PatchDigest:diff", NULL); } else { crm_trace("%p %.6x", digest_cs, digest_cs ? digest_cs->targets : 0); } } else { crm_trace("v%d digest matched: expected %s, calculated %s", format, digest, new_digest); } free(new_digest); free(version); } free_xml(old); return rc; } xmlNode * find_xml_node(xmlNode * root, const char *search_path, gboolean must_find) { xmlNode *a_child = NULL; const char *name = "NULL"; if (root != NULL) { name = crm_element_name(root); } if (search_path == NULL) { crm_warn("Will never find "); return NULL; } for (a_child = __xml_first_child(root); a_child != NULL; a_child = __xml_next(a_child)) { if (strcmp((const char *)a_child->name, search_path) == 0) { /* crm_trace("returning node (%s).", crm_element_name(a_child)); */ return a_child; } } if (must_find) { crm_warn("Could not find %s in %s.", search_path, name); } else if (root != NULL) { crm_trace("Could not find %s in %s.", search_path, name); } else { crm_trace("Could not find %s in .", search_path); } return NULL; } /* As the name suggests, the perfect match is required for both node name and fully specified attribute, otherwise, when attribute not specified, the outcome is the first node matching on the name. */ static xmlNode * find_entity_by_attr_or_just_name(xmlNode *parent, const char *node_name, const char *attr_n, const char *attr_v) { xmlNode *child; /* ensure attr_v specified when attr_n is */ CRM_CHECK(attr_n == NULL || attr_v != NULL, return NULL); for (child = __xml_first_child(parent); child != NULL; child = __xml_next(child)) { /* XXX uncertain if the first check is strictly necessary here */ if (node_name == NULL || !strcmp((const char *) child->name, node_name)) { if (attr_n == NULL || crm_str_eq(crm_element_value(child, attr_n), attr_v, TRUE)) { return child; } } } crm_trace("node <%s%s%s%s%s> not found in %s", crm_str(node_name), attr_n ? " " : "", attr_n ? attr_n : "", attr_n ? "=" : "", attr_n ? attr_v : "", crm_element_name(parent)); return NULL; } xmlNode * find_entity(xmlNode *parent, const char *node_name, const char *id) { return find_entity_by_attr_or_just_name(parent, node_name, (id == NULL) ? id : XML_ATTR_ID, id); } void copy_in_properties(xmlNode * target, xmlNode * src) { if (src == NULL) { crm_warn("No node to copy properties from"); } else if (target == NULL) { crm_err("No node to copy properties into"); } else { xmlAttrPtr pIter = NULL; for (pIter = pcmk__first_xml_attr(src); pIter != NULL; pIter = pIter->next) { const char *p_name = (const char *)pIter->name; const char *p_value = pcmk__xml_attr_value(pIter); expand_plus_plus(target, p_name, p_value); } } return; } void fix_plus_plus_recursive(xmlNode * target) { /* TODO: Remove recursion and use xpath searches for value++ */ xmlNode *child = NULL; xmlAttrPtr pIter = NULL; for (pIter = pcmk__first_xml_attr(target); pIter != NULL; pIter = pIter->next) { const char *p_name = (const char *)pIter->name; const char *p_value = pcmk__xml_attr_value(pIter); expand_plus_plus(target, p_name, p_value); } for (child = __xml_first_child(target); child != NULL; child = __xml_next(child)) { fix_plus_plus_recursive(child); } } void expand_plus_plus(xmlNode * target, const char *name, const char *value) { int offset = 1; int name_len = 0; int int_value = 0; int value_len = 0; const char *old_value = NULL; if (value == NULL || name == NULL) { return; } old_value = crm_element_value(target, name); if (old_value == NULL) { /* if no previous value, set unexpanded */ goto set_unexpanded; } else if (strstr(value, name) != value) { goto set_unexpanded; } name_len = strlen(name); value_len = strlen(value); if (value_len < (name_len + 2) || value[name_len] != '+' || (value[name_len + 1] != '+' && value[name_len + 1] != '=')) { goto set_unexpanded; } /* if we are expanding ourselves, * then no previous value was set and leave int_value as 0 */ if (old_value != value) { int_value = char2score(old_value); } if (value[name_len + 1] != '+') { const char *offset_s = value + (name_len + 2); offset = char2score(offset_s); } int_value += offset; if (int_value > INFINITY) { int_value = (int)INFINITY; } crm_xml_add_int(target, name, int_value); return; set_unexpanded: if (old_value == value) { /* the old value is already set, nothing to do */ return; } crm_xml_add(target, name, value); return; } xmlDoc * getDocPtr(xmlNode * node) { xmlDoc *doc = NULL; CRM_CHECK(node != NULL, return NULL); doc = node->doc; if (doc == NULL) { doc = xmlNewDoc((pcmkXmlStr) "1.0"); xmlDocSetRootElement(doc, node); xmlSetTreeDoc(node, doc); } return doc; } xmlNode * add_node_copy(xmlNode * parent, xmlNode * src_node) { xmlNode *child = NULL; xmlDoc *doc = getDocPtr(parent); CRM_CHECK(src_node != NULL, return NULL); child = xmlDocCopyNode(src_node, doc, 1); xmlAddChild(parent, child); crm_node_created(child); return child; } int add_node_nocopy(xmlNode * parent, const char *name, xmlNode * child) { add_node_copy(parent, child); free_xml(child); return 1; } xmlNode * create_xml_node(xmlNode * parent, const char *name) { xmlDoc *doc = NULL; xmlNode *node = NULL; if (name == NULL || name[0] == 0) { CRM_CHECK(name != NULL && name[0] == 0, return NULL); return NULL; } if (parent == NULL) { doc = xmlNewDoc((pcmkXmlStr) "1.0"); node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL); xmlDocSetRootElement(doc, node); } else { doc = getDocPtr(parent); node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL); xmlAddChild(parent, node); } crm_node_created(node); return node; } int pcmk__element_xpath(const char *prefix, xmlNode *xml, char *buffer, int offset, size_t buffer_size) { const char *id = ID(xml); if(offset == 0 && prefix == NULL && xml->parent) { offset = pcmk__element_xpath(NULL, xml->parent, buffer, offset, buffer_size); } if(id) { offset += snprintf(buffer + offset, buffer_size - offset, "/%s[@id='%s']", (const char *) xml->name, id); } else if(xml->name) { offset += snprintf(buffer + offset, buffer_size - offset, "/%s", (const char *) xml->name); } return offset; } char * xml_get_path(xmlNode *xml) { int offset = 0; char buffer[XML_BUFFER_SIZE]; if (pcmk__element_xpath(NULL, xml, buffer, offset, sizeof(buffer)) > 0) { return strdup(buffer); } return NULL; } +/*! + * Free an XML element and all of its children, removing it from its parent + * + * \param[in] xml XML element to free + */ +void +pcmk_free_xml_subtree(xmlNode *xml) +{ + xmlUnlinkNode(xml); // Detaches from parent and siblings + xmlFreeNode(xml); // Frees +} + static void free_xml_with_position(xmlNode * child, int position) { if (child != NULL) { xmlNode *top = NULL; xmlDoc *doc = child->doc; xml_private_t *p = child->_private; if (doc != NULL) { top = xmlDocGetRootElement(doc); } if (doc != NULL && top == child) { /* Free everything */ xmlFreeDoc(doc); } else if (pcmk__check_acl(child, NULL, xpf_acl_write) == FALSE) { int offset = 0; char buffer[XML_BUFFER_SIZE]; pcmk__element_xpath(NULL, child, buffer, offset, sizeof(buffer)); crm_trace("Cannot remove %s %x", buffer, p->flags); return; } else { if (doc && pcmk__tracking_xml_changes(child, FALSE) && is_not_set(p->flags, xpf_created)) { int offset = 0; char buffer[XML_BUFFER_SIZE]; if (pcmk__element_xpath(NULL, child, buffer, offset, sizeof(buffer)) > 0) { xml_deleted_obj_t *deleted_obj = calloc(1, sizeof(xml_deleted_obj_t)); crm_trace("Deleting %s %p from %p", buffer, child, doc); deleted_obj->path = strdup(buffer); deleted_obj->position = -1; /* Record the "position" only for XML comments for now */ if (child->type == XML_COMMENT_NODE) { if (position >= 0) { deleted_obj->position = position; } else { deleted_obj->position = __xml_offset(child); } } p = doc->_private; p->deleted_objs = g_list_append(p->deleted_objs, deleted_obj); pcmk__set_xml_flag(child, xpf_dirty); } } - - /* Free this particular subtree - * Make sure to unlink it from the parent first - */ - xmlUnlinkNode(child); - xmlFreeNode(child); + pcmk_free_xml_subtree(child); } } } void free_xml(xmlNode * child) { free_xml_with_position(child, -1); } xmlNode * copy_xml(xmlNode * src) { xmlDoc *doc = xmlNewDoc((pcmkXmlStr) "1.0"); xmlNode *copy = xmlDocCopyNode(src, doc, 1); xmlDocSetRootElement(doc, copy); xmlSetTreeDoc(copy, doc); return copy; } static void crm_xml_err(void *ctx, const char *fmt, ...) G_GNUC_PRINTF(2, 3); static void crm_xml_err(void *ctx, const char *fmt, ...) { va_list ap; static struct qb_log_callsite *xml_error_cs = NULL; if (xml_error_cs == NULL) { xml_error_cs = qb_log_callsite_get( __func__, __FILE__, "xml library error", LOG_TRACE, __LINE__, crm_trace_nonlog); } va_start(ap, fmt); if (xml_error_cs && xml_error_cs->targets) { CRM_XML_LOG_BASE(LOG_ERR, TRUE, crm_abort(__FILE__, __PRETTY_FUNCTION__, __LINE__, "xml library error", TRUE, TRUE), "XML Error: ", fmt, ap); } else { CRM_XML_LOG_BASE(LOG_ERR, TRUE, 0, "XML Error: ", fmt, ap); } va_end(ap); } xmlNode * string2xml(const char *input) { xmlNode *xml = NULL; xmlDocPtr output = NULL; xmlParserCtxtPtr ctxt = NULL; xmlErrorPtr last_error = NULL; if (input == NULL) { crm_err("Can't parse NULL input"); return NULL; } /* create a parser context */ ctxt = xmlNewParserCtxt(); CRM_CHECK(ctxt != NULL, return NULL); /* xmlCtxtUseOptions(ctxt, XML_PARSE_NOBLANKS|XML_PARSE_RECOVER); */ xmlCtxtResetLastError(ctxt); xmlSetGenericErrorFunc(ctxt, crm_xml_err); /* initGenericErrorDefaultFunc(crm_xml_err); */ output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, XML_PARSE_NOBLANKS | XML_PARSE_RECOVER); if (output) { xml = xmlDocGetRootElement(output); } last_error = xmlCtxtGetLastError(ctxt); if (last_error && last_error->code != XML_ERR_OK) { /* crm_abort(__FILE__,__FUNCTION__,__LINE__, "last_error->code != XML_ERR_OK", TRUE, TRUE); */ /* * http://xmlsoft.org/html/libxml-xmlerror.html#xmlErrorLevel * http://xmlsoft.org/html/libxml-xmlerror.html#xmlParserErrors */ crm_warn("Parsing failed (domain=%d, level=%d, code=%d): %s", last_error->domain, last_error->level, last_error->code, last_error->message); if (last_error->code == XML_ERR_DOCUMENT_EMPTY) { CRM_LOG_ASSERT("Cannot parse an empty string"); } else if (last_error->code != XML_ERR_DOCUMENT_END) { crm_err("Couldn't%s parse %d chars: %s", xml ? " fully" : "", (int)strlen(input), input); if (xml != NULL) { crm_log_xml_err(xml, "Partial"); } } else { int len = strlen(input); int lpc = 0; while(lpc < len) { crm_warn("Parse error[+%.3d]: %.80s", lpc, input+lpc); lpc += 80; } CRM_LOG_ASSERT("String parsing error"); } } xmlFreeParserCtxt(ctxt); return xml; } xmlNode * stdin2xml(void) { size_t data_length = 0; size_t read_chars = 0; char *xml_buffer = NULL; xmlNode *xml_obj = NULL; do { xml_buffer = realloc_safe(xml_buffer, data_length + XML_BUFFER_SIZE); read_chars = fread(xml_buffer + data_length, 1, XML_BUFFER_SIZE, stdin); data_length += read_chars; } while (read_chars == XML_BUFFER_SIZE); if (data_length == 0) { crm_warn("No XML supplied on stdin"); free(xml_buffer); return NULL; } xml_buffer[data_length] = '\0'; xml_obj = string2xml(xml_buffer); free(xml_buffer); crm_log_xml_trace(xml_obj, "Created fragment"); return xml_obj; } static char * decompress_file(const char *filename) { char *buffer = NULL; #if HAVE_BZLIB_H int rc = 0; size_t length = 0, read_len = 0; BZFILE *bz_file = NULL; FILE *input = fopen(filename, "r"); if (input == NULL) { crm_perror(LOG_ERR, "Could not open %s for reading", filename); return NULL; } bz_file = BZ2_bzReadOpen(&rc, input, 0, 0, NULL, 0); if (rc != BZ_OK) { crm_err("Could not prepare to read compressed %s: %s " CRM_XS " bzerror=%d", filename, bz2_strerror(rc), rc); BZ2_bzReadClose(&rc, bz_file); return NULL; } rc = BZ_OK; while (rc == BZ_OK) { buffer = realloc_safe(buffer, XML_BUFFER_SIZE + length + 1); read_len = BZ2_bzRead(&rc, bz_file, buffer + length, XML_BUFFER_SIZE); crm_trace("Read %ld bytes from file: %d", (long)read_len, rc); if (rc == BZ_OK || rc == BZ_STREAM_END) { length += read_len; } } buffer[length] = '\0'; if (rc != BZ_STREAM_END) { crm_err("Could not read compressed %s: %s " CRM_XS " bzerror=%d", filename, bz2_strerror(rc), rc); free(buffer); buffer = NULL; } BZ2_bzReadClose(&rc, bz_file); fclose(input); #else crm_err("Could not read compressed %s: not built with bzlib support", filename); #endif return buffer; } void strip_text_nodes(xmlNode * xml) { xmlNode *iter = xml->children; while (iter) { xmlNode *next = iter->next; switch (iter->type) { case XML_TEXT_NODE: /* Remove it */ - xmlUnlinkNode(iter); - xmlFreeNode(iter); + pcmk_free_xml_subtree(iter); break; case XML_ELEMENT_NODE: /* Search it */ strip_text_nodes(iter); break; default: /* Leave it */ break; } iter = next; } } xmlNode * filename2xml(const char *filename) { xmlNode *xml = NULL; xmlDocPtr output = NULL; gboolean uncompressed = TRUE; xmlParserCtxtPtr ctxt = NULL; xmlErrorPtr last_error = NULL; static int xml_options = XML_PARSE_NOBLANKS | XML_PARSE_RECOVER; /* create a parser context */ ctxt = xmlNewParserCtxt(); CRM_CHECK(ctxt != NULL, return NULL); /* xmlCtxtUseOptions(ctxt, XML_PARSE_NOBLANKS|XML_PARSE_RECOVER); */ xmlCtxtResetLastError(ctxt); xmlSetGenericErrorFunc(ctxt, crm_xml_err); /* initGenericErrorDefaultFunc(crm_xml_err); */ if (filename) { uncompressed = !crm_ends_with_ext(filename, ".bz2"); } if (filename == NULL) { /* STDIN_FILENO == fileno(stdin) */ output = xmlCtxtReadFd(ctxt, STDIN_FILENO, "unknown.xml", NULL, xml_options); } else if (uncompressed) { output = xmlCtxtReadFile(ctxt, filename, NULL, xml_options); } else { char *input = decompress_file(filename); output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, xml_options); free(input); } if (output && (xml = xmlDocGetRootElement(output))) { strip_text_nodes(xml); } last_error = xmlCtxtGetLastError(ctxt); if (last_error && last_error->code != XML_ERR_OK) { /* crm_abort(__FILE__,__FUNCTION__,__LINE__, "last_error->code != XML_ERR_OK", TRUE, TRUE); */ /* * http://xmlsoft.org/html/libxml-xmlerror.html#xmlErrorLevel * http://xmlsoft.org/html/libxml-xmlerror.html#xmlParserErrors */ crm_err("Parsing failed (domain=%d, level=%d, code=%d): %s", last_error->domain, last_error->level, last_error->code, last_error->message); if (last_error && last_error->code != XML_ERR_OK) { crm_err("Couldn't%s parse %s", xml ? " fully" : "", filename); if (xml != NULL) { crm_log_xml_err(xml, "Partial"); } } } xmlFreeParserCtxt(ctxt); return xml; } /*! * \internal * \brief Add a "last written" attribute to an XML node, set to current time * * \param[in] xml_node XML node to get attribute * * \return Value that was set, or NULL on error */ const char * crm_xml_add_last_written(xmlNode *xml_node) { const char *now_str = crm_now_string(); return crm_xml_add(xml_node, XML_CIB_ATTR_WRITTEN, now_str); } /*! * \brief Sanitize a string so it is usable as an XML ID * * \param[in,out] id String to sanitize */ void crm_xml_sanitize_id(char *id) { char *c; for (c = id; *c; ++c) { /* @TODO Sanitize more comprehensively */ switch (*c) { case ':': case '#': *c = '.'; } } } /*! * \brief Set the ID of an XML element using a format * * \param[in,out] xml XML element * \param[in] fmt printf-style format * \param[in] ... any arguments required by format */ void crm_xml_set_id(xmlNode *xml, const char *format, ...) { va_list ap; int len = 0; char *id = NULL; /* equivalent to crm_strdup_printf() */ va_start(ap, format); len = vasprintf(&id, format, ap); va_end(ap); CRM_ASSERT(len > 0); crm_xml_sanitize_id(id); crm_xml_add(xml, XML_ATTR_ID, id); free(id); } /*! * \internal * \brief Write XML to a file stream * * \param[in] xml_node XML to write * \param[in] filename Name of file being written (for logging only) * \param[in] stream Open file stream corresponding to filename * \param[in] compress Whether to compress XML before writing * * \return Number of bytes written on success, -errno otherwise */ static int write_xml_stream(xmlNode * xml_node, const char *filename, FILE * stream, gboolean compress) { int res = 0; char *buffer = NULL; unsigned int out = 0; crm_log_xml_trace(xml_node, "writing"); buffer = dump_xml_formatted(xml_node); CRM_CHECK(buffer && strlen(buffer), crm_log_xml_warn(xml_node, "formatting failed"); res = -pcmk_err_generic; goto bail); if (compress) { #if HAVE_BZLIB_H int rc = BZ_OK; unsigned int in = 0; BZFILE *bz_file = NULL; bz_file = BZ2_bzWriteOpen(&rc, stream, 5, 0, 30); if (rc != BZ_OK) { crm_warn("Not compressing %s: could not prepare file stream: %s " CRM_XS " bzerror=%d", filename, bz2_strerror(rc), rc); } else { BZ2_bzWrite(&rc, bz_file, buffer, strlen(buffer)); if (rc != BZ_OK) { crm_warn("Not compressing %s: could not compress data: %s " CRM_XS " bzerror=%d errno=%d", filename, bz2_strerror(rc), rc, errno); } } if (rc == BZ_OK) { BZ2_bzWriteClose(&rc, bz_file, 0, &in, &out); if (rc != BZ_OK) { crm_warn("Not compressing %s: could not write compressed data: %s " CRM_XS " bzerror=%d errno=%d", filename, bz2_strerror(rc), rc, errno); out = 0; // retry without compression } else { res = (int) out; crm_trace("Compressed XML for %s from %u bytes to %u", filename, in, out); } } #else crm_warn("Not compressing %s: not built with bzlib support", filename); #endif } if (out == 0) { res = fprintf(stream, "%s", buffer); if (res < 0) { res = -errno; crm_perror(LOG_ERR, "writing %s", filename); goto bail; } } bail: if (fflush(stream) != 0) { res = -errno; crm_perror(LOG_ERR, "flushing %s", filename); } /* Don't report error if the file does not support synchronization */ if (fsync(fileno(stream)) < 0 && errno != EROFS && errno != EINVAL) { res = -errno; crm_perror(LOG_ERR, "synchronizing %s", filename); } fclose(stream); crm_trace("Saved %d bytes%s to %s as XML", res, ((out > 0)? " (compressed)" : ""), filename); free(buffer); return res; } /*! * \brief Write XML to a file descriptor * * \param[in] xml_node XML to write * \param[in] filename Name of file being written (for logging only) * \param[in] fd Open file descriptor corresponding to filename * \param[in] compress Whether to compress XML before writing * * \return Number of bytes written on success, -errno otherwise */ int write_xml_fd(xmlNode * xml_node, const char *filename, int fd, gboolean compress) { FILE *stream = NULL; CRM_CHECK(xml_node && (fd > 0), return -EINVAL); stream = fdopen(fd, "w"); if (stream == NULL) { return -errno; } return write_xml_stream(xml_node, filename, stream, compress); } /*! * \brief Write XML to a file * * \param[in] xml_node XML to write * \param[in] filename Name of file to write * \param[in] compress Whether to compress XML before writing * * \return Number of bytes written on success, -errno otherwise */ int write_xml_file(xmlNode * xml_node, const char *filename, gboolean compress) { FILE *stream = NULL; CRM_CHECK(xml_node && filename, return -EINVAL); stream = fopen(filename, "w"); if (stream == NULL) { return -errno; } return write_xml_stream(xml_node, filename, stream, compress); } xmlNode * get_message_xml(xmlNode * msg, const char *field) { xmlNode *tmp = first_named_child(msg, field); return __xml_first_child(tmp); } gboolean add_message_xml(xmlNode * msg, const char *field, xmlNode * xml) { xmlNode *holder = create_xml_node(msg, field); add_node_copy(holder, xml); return TRUE; } static char * crm_xml_escape_shuffle(char *text, int start, int *length, const char *replace) { int lpc; int offset = strlen(replace) - 1; /* We have space for 1 char already */ *length += offset; text = realloc_safe(text, *length); for (lpc = (*length) - 1; lpc > (start + offset); lpc--) { text[lpc] = text[lpc - offset]; } memcpy(text + start, replace, offset + 1); return text; } char * crm_xml_escape(const char *text) { int index; int changes = 0; int length = 1 + strlen(text); char *copy = strdup(text); /* * When xmlCtxtReadDoc() parses < and friends in a * value, it converts them to their human readable * form. * * If one uses xmlNodeDump() to convert it back to a * string, all is well, because special characters are * converted back to their escape sequences. * * However xmlNodeDump() is randomly dog slow, even with the same * input. So we need to replicate the escaping in our custom * version so that the result can be re-parsed by xmlCtxtReadDoc() * when necessary. */ for (index = 0; index < length; index++) { switch (copy[index]) { case 0: break; case '<': copy = crm_xml_escape_shuffle(copy, index, &length, "<"); changes++; break; case '>': copy = crm_xml_escape_shuffle(copy, index, &length, ">"); changes++; break; case '"': copy = crm_xml_escape_shuffle(copy, index, &length, """); changes++; break; case '\'': copy = crm_xml_escape_shuffle(copy, index, &length, "'"); changes++; break; case '&': copy = crm_xml_escape_shuffle(copy, index, &length, "&"); changes++; break; case '\t': /* Might as well just expand to a few spaces... */ copy = crm_xml_escape_shuffle(copy, index, &length, " "); changes++; break; case '\n': /* crm_trace("Convert: \\%.3o", copy[index]); */ copy = crm_xml_escape_shuffle(copy, index, &length, "\\n"); changes++; break; case '\r': copy = crm_xml_escape_shuffle(copy, index, &length, "\\r"); changes++; break; /* For debugging... case '\\': crm_trace("Passthrough: \\%c", copy[index+1]); break; */ default: /* Check for and replace non-printing characters with their octal equivalent */ if(copy[index] < ' ' || copy[index] > '~') { char *replace = crm_strdup_printf("\\%.3o", copy[index]); /* crm_trace("Convert to octal: \\%.3o", copy[index]); */ copy = crm_xml_escape_shuffle(copy, index, &length, replace); free(replace); changes++; } } } if (changes) { crm_trace("Dumped '%s'", copy); } return copy; } static inline void dump_xml_attr(xmlAttrPtr attr, int options, char **buffer, int *offset, int *max) { char *p_value = NULL; const char *p_name = NULL; xml_private_t *p = NULL; CRM_ASSERT(buffer != NULL); if (attr == NULL || attr->children == NULL) { return; } p = attr->_private; if (p && is_set(p->flags, xpf_deleted)) { return; } p_name = (const char *)attr->name; p_value = crm_xml_escape((const char *)attr->children->content); buffer_print(*buffer, *max, *offset, " %s=\"%s\"", p_name, p_value); free(p_value); } static void __xml_log_element(int log_level, const char *file, const char *function, int line, const char *prefix, xmlNode * data, int depth, int options) { int max = 0; int offset = 0; const char *name = NULL; const char *hidden = NULL; xmlNode *child = NULL; xmlAttrPtr pIter = NULL; if(data == NULL) { return; } name = crm_element_name(data); if(is_set(options, xml_log_option_open)) { char *buffer = NULL; insert_prefix(options, &buffer, &offset, &max, depth); if (data->type == XML_COMMENT_NODE) { buffer_print(buffer, max, offset, "", data->content); } else { buffer_print(buffer, max, offset, "<%s", name); hidden = crm_element_value(data, "hidden"); for (pIter = pcmk__first_xml_attr(data); pIter != NULL; pIter = pIter->next) { xml_private_t *p = pIter->_private; const char *p_name = (const char *)pIter->name; const char *p_value = pcmk__xml_attr_value(pIter); char *p_copy = NULL; if(is_set(p->flags, xpf_deleted)) { continue; } else if ((is_set(options, xml_log_option_diff_plus) || is_set(options, xml_log_option_diff_minus)) && strcmp(XML_DIFF_MARKER, p_name) == 0) { continue; } else if (hidden != NULL && p_name[0] != 0 && strstr(hidden, p_name) != NULL) { p_copy = strdup("*****"); } else { p_copy = crm_xml_escape(p_value); } buffer_print(buffer, max, offset, " %s=\"%s\"", p_name, p_copy); free(p_copy); } if(xml_has_children(data) == FALSE) { buffer_print(buffer, max, offset, "/>"); } else if(is_set(options, xml_log_option_children)) { buffer_print(buffer, max, offset, ">"); } else { buffer_print(buffer, max, offset, "/>"); } } do_crm_log_alias(log_level, file, function, line, "%s %s", prefix, buffer); free(buffer); } if(data->type == XML_COMMENT_NODE) { return; } else if(xml_has_children(data) == FALSE) { return; } else if(is_set(options, xml_log_option_children)) { offset = 0; max = 0; for (child = __xml_first_child(data); child != NULL; child = __xml_next(child)) { __xml_log_element(log_level, file, function, line, prefix, child, depth + 1, options|xml_log_option_open|xml_log_option_close); } } if(is_set(options, xml_log_option_close)) { char *buffer = NULL; insert_prefix(options, &buffer, &offset, &max, depth); buffer_print(buffer, max, offset, "", name); do_crm_log_alias(log_level, file, function, line, "%s %s", prefix, buffer); free(buffer); } } static void __xml_log_change_element(int log_level, const char *file, const char *function, int line, const char *prefix, xmlNode * data, int depth, int options) { xml_private_t *p; char *prefix_m = NULL; xmlNode *child = NULL; xmlAttrPtr pIter = NULL; if(data == NULL) { return; } p = data->_private; prefix_m = strdup(prefix); prefix_m[1] = '+'; if(is_set(p->flags, xpf_dirty) && is_set(p->flags, xpf_created)) { /* Continue and log full subtree */ __xml_log_element(log_level, file, function, line, prefix_m, data, depth, options|xml_log_option_open|xml_log_option_close|xml_log_option_children); } else if(is_set(p->flags, xpf_dirty)) { char *spaces = calloc(80, 1); int s_count = 0, s_max = 80; char *prefix_del = NULL; char *prefix_moved = NULL; const char *flags = prefix; insert_prefix(options, &spaces, &s_count, &s_max, depth); prefix_del = strdup(prefix); prefix_del[0] = '-'; prefix_del[1] = '-'; prefix_moved = strdup(prefix); prefix_moved[1] = '~'; if(is_set(p->flags, xpf_moved)) { flags = prefix_moved; } else { flags = prefix; } __xml_log_element(log_level, file, function, line, flags, data, depth, options|xml_log_option_open); for (pIter = pcmk__first_xml_attr(data); pIter != NULL; pIter = pIter->next) { const char *aname = (const char*)pIter->name; p = pIter->_private; if(is_set(p->flags, xpf_deleted)) { const char *value = crm_element_value(data, aname); flags = prefix_del; do_crm_log_alias(log_level, file, function, line, "%s %s @%s=%s", flags, spaces, aname, value); } else if(is_set(p->flags, xpf_dirty)) { const char *value = crm_element_value(data, aname); if(is_set(p->flags, xpf_created)) { flags = prefix_m; } else if(is_set(p->flags, xpf_modified)) { flags = prefix; } else if(is_set(p->flags, xpf_moved)) { flags = prefix_moved; } else { flags = prefix; } do_crm_log_alias(log_level, file, function, line, "%s %s @%s=%s", flags, spaces, aname, value); } } free(prefix_moved); free(prefix_del); free(spaces); for (child = __xml_first_child(data); child != NULL; child = __xml_next(child)) { __xml_log_change_element(log_level, file, function, line, prefix, child, depth + 1, options); } __xml_log_element(log_level, file, function, line, prefix, data, depth, options|xml_log_option_close); } else { for (child = __xml_first_child(data); child != NULL; child = __xml_next(child)) { __xml_log_change_element(log_level, file, function, line, prefix, child, depth + 1, options); } } free(prefix_m); } void log_data_element(int log_level, const char *file, const char *function, int line, const char *prefix, xmlNode * data, int depth, int options) { xmlNode *a_child = NULL; char *prefix_m = NULL; if (prefix == NULL) { prefix = ""; } /* Since we use the same file and line, to avoid confusing libqb, we need to use the same format strings */ if (data == NULL) { do_crm_log_alias(log_level, file, function, line, "%s: %s", prefix, "No data to dump as XML"); return; } if(is_set(options, xml_log_option_dirty_add) || is_set(options, xml_log_option_dirty_add)) { __xml_log_change_element(log_level, file, function, line, prefix, data, depth, options); return; } if (is_set(options, xml_log_option_formatted)) { if (is_set(options, xml_log_option_diff_plus) && (data->children == NULL || crm_element_value(data, XML_DIFF_MARKER))) { options |= xml_log_option_diff_all; prefix_m = strdup(prefix); prefix_m[1] = '+'; prefix = prefix_m; } else if (is_set(options, xml_log_option_diff_minus) && (data->children == NULL || crm_element_value(data, XML_DIFF_MARKER))) { options |= xml_log_option_diff_all; prefix_m = strdup(prefix); prefix_m[1] = '-'; prefix = prefix_m; } } if (is_set(options, xml_log_option_diff_short) && is_not_set(options, xml_log_option_diff_all)) { /* Still searching for the actual change */ for (a_child = __xml_first_child(data); a_child != NULL; a_child = __xml_next(a_child)) { log_data_element(log_level, file, function, line, prefix, a_child, depth + 1, options); } } else { __xml_log_element(log_level, file, function, line, prefix, data, depth, options|xml_log_option_open|xml_log_option_close|xml_log_option_children); } free(prefix_m); } static void dump_filtered_xml(xmlNode * data, int options, char **buffer, int *offset, int *max) { int lpc; xmlAttrPtr xIter = NULL; static int filter_len = DIMOF(filter); for (lpc = 0; options && lpc < filter_len; lpc++) { filter[lpc].found = FALSE; } for (xIter = pcmk__first_xml_attr(data); xIter != NULL; xIter = xIter->next) { bool skip = FALSE; const char *p_name = (const char *)xIter->name; for (lpc = 0; skip == FALSE && lpc < filter_len; lpc++) { if (filter[lpc].found == FALSE && strcmp(p_name, filter[lpc].string) == 0) { filter[lpc].found = TRUE; skip = TRUE; break; } } if (skip == FALSE) { dump_xml_attr(xIter, options, buffer, offset, max); } } } static void dump_xml_element(xmlNode * data, int options, char **buffer, int *offset, int *max, int depth) { const char *name = NULL; CRM_ASSERT(max != NULL); CRM_ASSERT(offset != NULL); CRM_ASSERT(buffer != NULL); if (data == NULL) { crm_trace("Nothing to dump"); return; } if (*buffer == NULL) { *offset = 0; *max = 0; } name = crm_element_name(data); CRM_ASSERT(name != NULL); insert_prefix(options, buffer, offset, max, depth); buffer_print(*buffer, *max, *offset, "<%s", name); if (options & xml_log_option_filtered) { dump_filtered_xml(data, options, buffer, offset, max); } else { xmlAttrPtr xIter = NULL; for (xIter = pcmk__first_xml_attr(data); xIter != NULL; xIter = xIter->next) { dump_xml_attr(xIter, options, buffer, offset, max); } } if (data->children == NULL) { buffer_print(*buffer, *max, *offset, "/>"); } else { buffer_print(*buffer, *max, *offset, ">"); } if (options & xml_log_option_formatted) { buffer_print(*buffer, *max, *offset, "\n"); } if (data->children) { xmlNode *xChild = NULL; for(xChild = data->children; xChild != NULL; xChild = xChild->next) { crm_xml_dump(xChild, options, buffer, offset, max, depth + 1); } insert_prefix(options, buffer, offset, max, depth); buffer_print(*buffer, *max, *offset, "", name); if (options & xml_log_option_formatted) { buffer_print(*buffer, *max, *offset, "\n"); } } } static void dump_xml_text(xmlNode * data, int options, char **buffer, int *offset, int *max, int depth) { CRM_ASSERT(max != NULL); CRM_ASSERT(offset != NULL); CRM_ASSERT(buffer != NULL); if (data == NULL) { crm_trace("Nothing to dump"); return; } if (*buffer == NULL) { *offset = 0; *max = 0; } insert_prefix(options, buffer, offset, max, depth); buffer_print(*buffer, *max, *offset, "%s", data->content); if (options & xml_log_option_formatted) { buffer_print(*buffer, *max, *offset, "\n"); } } static void dump_xml_cdata(xmlNode * data, int options, char **buffer, int *offset, int *max, int depth) { CRM_ASSERT(max != NULL); CRM_ASSERT(offset != NULL); CRM_ASSERT(buffer != NULL); if (data == NULL) { crm_trace("Nothing to dump"); return; } if (*buffer == NULL) { *offset = 0; *max = 0; } insert_prefix(options, buffer, offset, max, depth); buffer_print(*buffer, *max, *offset, "content); buffer_print(*buffer, *max, *offset, "]]>"); if (options & xml_log_option_formatted) { buffer_print(*buffer, *max, *offset, "\n"); } } static void dump_xml_comment(xmlNode * data, int options, char **buffer, int *offset, int *max, int depth) { CRM_ASSERT(max != NULL); CRM_ASSERT(offset != NULL); CRM_ASSERT(buffer != NULL); if (data == NULL) { crm_trace("Nothing to dump"); return; } if (*buffer == NULL) { *offset = 0; *max = 0; } insert_prefix(options, buffer, offset, max, depth); buffer_print(*buffer, *max, *offset, ""); if (options & xml_log_option_formatted) { buffer_print(*buffer, *max, *offset, "\n"); } } void crm_xml_dump(xmlNode * data, int options, char **buffer, int *offset, int *max, int depth) { if(data == NULL) { *offset = 0; *max = 0; return; } #if 0 if (is_not_set(options, xml_log_option_filtered)) { /* Turning this code on also changes the scheduler tests for some reason * (not just newlines). Figure out why before considering to * enable this permanently. * * It exists to help debug slowness in xmlNodeDump() and * potentially if we ever want to go back to it. * * In theory it's a good idea (reuse) but our custom version does * better for the filtered case and avoids the final strdup() for * everything */ time_t now, next; xmlDoc *doc = NULL; xmlBuffer *xml_buffer = NULL; *buffer = NULL; doc = getDocPtr(data); /* doc will only be NULL if data is */ CRM_CHECK(doc != NULL, return); now = time(NULL); xml_buffer = xmlBufferCreate(); CRM_ASSERT(xml_buffer != NULL); /* The default allocator XML_BUFFER_ALLOC_EXACT does far too many * realloc()s and it can take upwards of 18 seconds (yes, seconds) * to dump a 28kb tree which XML_BUFFER_ALLOC_DOUBLEIT can do in * less than 1 second. * * We could also use xmlBufferCreateSize() to start with a * sane-ish initial size and avoid the first few doubles. */ xmlBufferSetAllocationScheme(xml_buffer, XML_BUFFER_ALLOC_DOUBLEIT); *max = xmlNodeDump(xml_buffer, doc, data, 0, (options & xml_log_option_formatted)); if (*max > 0) { *buffer = strdup((char *)xml_buffer->content); } next = time(NULL); if ((now + 1) < next) { crm_log_xml_trace(data, "Long time"); crm_err("xmlNodeDump() -> %dbytes took %ds", *max, next - now); } xmlBufferFree(xml_buffer); return; } #endif switch(data->type) { case XML_ELEMENT_NODE: /* Handle below */ dump_xml_element(data, options, buffer, offset, max, depth); break; case XML_TEXT_NODE: /* if option xml_log_option_text is enabled, then dump XML_TEXT_NODE */ if (options & xml_log_option_text) { dump_xml_text(data, options, buffer, offset, max, depth); } return; case XML_COMMENT_NODE: dump_xml_comment(data, options, buffer, offset, max, depth); break; case XML_CDATA_SECTION_NODE: dump_xml_cdata(data, options, buffer, offset, max, depth); break; default: crm_warn("Unhandled type: %d", data->type); return; /* XML_ATTRIBUTE_NODE = 2 XML_ENTITY_REF_NODE = 5 XML_ENTITY_NODE = 6 XML_PI_NODE = 7 XML_DOCUMENT_NODE = 9 XML_DOCUMENT_TYPE_NODE = 10 XML_DOCUMENT_FRAG_NODE = 11 XML_NOTATION_NODE = 12 XML_HTML_DOCUMENT_NODE = 13 XML_DTD_NODE = 14 XML_ELEMENT_DECL = 15 XML_ATTRIBUTE_DECL = 16 XML_ENTITY_DECL = 17 XML_NAMESPACE_DECL = 18 XML_XINCLUDE_START = 19 XML_XINCLUDE_END = 20 XML_DOCB_DOCUMENT_NODE = 21 */ } } void crm_buffer_add_char(char **buffer, int *offset, int *max, char c) { buffer_print(*buffer, *max, *offset, "%c", c); } char * dump_xml_formatted_with_text(xmlNode * an_xml_node) { char *buffer = NULL; int offset = 0, max = 0; crm_xml_dump(an_xml_node, xml_log_option_formatted|xml_log_option_text, &buffer, &offset, &max, 0); return buffer; } char * dump_xml_formatted(xmlNode * an_xml_node) { char *buffer = NULL; int offset = 0, max = 0; crm_xml_dump(an_xml_node, xml_log_option_formatted, &buffer, &offset, &max, 0); return buffer; } char * dump_xml_unformatted(xmlNode * an_xml_node) { char *buffer = NULL; int offset = 0, max = 0; crm_xml_dump(an_xml_node, 0, &buffer, &offset, &max, 0); return buffer; } gboolean xml_has_children(const xmlNode * xml_root) { if (xml_root != NULL && xml_root->children != NULL) { return TRUE; } return FALSE; } void xml_remove_prop(xmlNode * obj, const char *name) { if (pcmk__check_acl(obj, NULL, xpf_acl_write) == FALSE) { crm_trace("Cannot remove %s from %s", name, obj->name); } else if (pcmk__tracking_xml_changes(obj, FALSE)) { /* Leave in place (marked for removal) until after the diff is calculated */ xml_private_t *p = NULL; xmlAttr *attr = xmlHasProp(obj, (pcmkXmlStr) name); p = attr->_private; set_parent_flag(obj, xpf_dirty); p->flags |= xpf_deleted; /* crm_trace("Setting flag %x due to %s[@id=%s].%s", xpf_dirty, obj->name, ID(obj), name); */ } else { xmlUnsetProp(obj, (pcmkXmlStr) name); } } void purge_diff_markers(xmlNode * a_node) { xmlNode *child = NULL; CRM_CHECK(a_node != NULL, return); xml_remove_prop(a_node, XML_DIFF_MARKER); for (child = __xml_first_child(a_node); child != NULL; child = __xml_next(child)) { purge_diff_markers(child); } } void save_xml_to_file(xmlNode * xml, const char *desc, const char *filename) { char *f = NULL; if (filename == NULL) { char *uuid = crm_generate_uuid(); f = crm_strdup_printf("%s/%s", crm_get_tmpdir(), uuid); filename = f; free(uuid); } crm_info("Saving %s to %s", desc, filename); write_xml_file(xml, filename, FALSE); free(f); } gboolean apply_xml_diff(xmlNode *old_xml, xmlNode * diff, xmlNode **new_xml) { gboolean result = TRUE; int root_nodes_seen = 0; static struct qb_log_callsite *digest_cs = NULL; const char *digest = crm_element_value(diff, XML_ATTR_DIGEST); const char *version = crm_element_value(diff, XML_ATTR_CRM_VERSION); xmlNode *child_diff = NULL; xmlNode *added = find_xml_node(diff, "diff-added", FALSE); xmlNode *removed = find_xml_node(diff, "diff-removed", FALSE); CRM_CHECK(new_xml != NULL, return FALSE); if (digest_cs == NULL) { digest_cs = qb_log_callsite_get(__func__, __FILE__, "diff-digest", LOG_TRACE, __LINE__, crm_trace_nonlog); } crm_trace("Subtraction Phase"); for (child_diff = __xml_first_child(removed); child_diff != NULL; child_diff = __xml_next(child_diff)) { CRM_CHECK(root_nodes_seen == 0, result = FALSE); if (root_nodes_seen == 0) { *new_xml = subtract_xml_object(NULL, old_xml, child_diff, FALSE, NULL, NULL); } root_nodes_seen++; } if (root_nodes_seen == 0) { *new_xml = copy_xml(old_xml); } else if (root_nodes_seen > 1) { crm_err("(-) Diffs cannot contain more than one change set..." " saw %d", root_nodes_seen); result = FALSE; } root_nodes_seen = 0; crm_trace("Addition Phase"); if (result) { xmlNode *child_diff = NULL; for (child_diff = __xml_first_child(added); child_diff != NULL; child_diff = __xml_next(child_diff)) { CRM_CHECK(root_nodes_seen == 0, result = FALSE); if (root_nodes_seen == 0) { add_xml_object(NULL, *new_xml, child_diff, TRUE); } root_nodes_seen++; } } if (root_nodes_seen > 1) { crm_err("(+) Diffs cannot contain more than one change set..." " saw %d", root_nodes_seen); result = FALSE; } else if (result && digest) { char *new_digest = NULL; purge_diff_markers(*new_xml); /* Purge now so the diff is ok */ new_digest = calculate_xml_versioned_digest(*new_xml, FALSE, TRUE, version); if (safe_str_neq(new_digest, digest)) { crm_info("Digest mis-match: expected %s, calculated %s", digest, new_digest); result = FALSE; crm_trace("%p %.6x", digest_cs, digest_cs ? digest_cs->targets : 0); if (digest_cs && digest_cs->targets) { save_xml_to_file(old_xml, "diff:original", NULL); save_xml_to_file(diff, "diff:input", NULL); save_xml_to_file(*new_xml, "diff:new", NULL); } } else { crm_trace("Digest matched: expected %s, calculated %s", digest, new_digest); } free(new_digest); } else if (result) { purge_diff_markers(*new_xml); /* Purge now so the diff is ok */ } return result; } static void -__xml_diff_object(xmlNode *old_xml, xmlNode *new_xml) +__xml_diff_object(xmlNode *old_xml, xmlNode *new_xml, bool check_top) { xmlNode *cIter = NULL; xmlAttr *pIter = NULL; CRM_CHECK(new_xml != NULL, return); if (old_xml == NULL) { crm_node_created(new_xml); - pcmk__post_process_acl(new_xml); // Check creation is allowed + pcmk__apply_creation_acl(new_xml, check_top); return; } else { xml_private_t *p = new_xml->_private; if(p->flags & xpf_processed) { /* Avoid re-comparing nodes */ return; } p->flags |= xpf_processed; } for (pIter = pcmk__first_xml_attr(new_xml); pIter != NULL; pIter = pIter->next) { xml_private_t *p = pIter->_private; /* Assume everything was just created and take it from there */ p->flags |= xpf_created; } for (pIter = pcmk__first_xml_attr(old_xml); pIter != NULL; ) { xmlAttr *prop = pIter; xml_private_t *p = NULL; const char *name = (const char *)pIter->name; const char *old_value = crm_element_value(old_xml, name); xmlAttr *exists = xmlHasProp(new_xml, pIter->name); pIter = pIter->next; if(exists == NULL) { p = new_xml->doc->_private; /* Prevent the dirty flag being set recursively upwards */ clear_bit(p->flags, xpf_tracking); exists = xmlSetProp(new_xml, (pcmkXmlStr) name, (pcmkXmlStr) old_value); set_bit(p->flags, xpf_tracking); p = exists->_private; p->flags = 0; crm_trace("Lost %s@%s=%s", old_xml->name, name, old_value); xml_remove_prop(new_xml, name); } else { int p_new = __xml_offset((xmlNode*)exists); int p_old = __xml_offset((xmlNode*)prop); const char *value = crm_element_value(new_xml, name); p = exists->_private; p->flags = (p->flags & ~xpf_created); if(strcmp(value, old_value) != 0) { /* Restore the original value, so we can call crm_xml_add(), * which checks ACLs */ char *vcopy = crm_element_value_copy(new_xml, name); crm_trace("Modified %s@%s %s->%s", old_xml->name, name, old_value, vcopy); xmlSetProp(new_xml, prop->name, (pcmkXmlStr) old_value); crm_xml_add(new_xml, name, vcopy); free(vcopy); } else if ((p_old != p_new) && !pcmk__tracking_xml_changes(new_xml, TRUE)) { crm_info("Moved %s@%s (%d -> %d)", old_xml->name, name, p_old, p_new); __xml_node_dirty(new_xml); p->flags |= xpf_dirty|xpf_moved; if(p_old > p_new) { p = prop->_private; p->flags |= xpf_skip; } else { p = exists->_private; p->flags |= xpf_skip; } } } } for (pIter = pcmk__first_xml_attr(new_xml); pIter != NULL; ) { xmlAttr *prop = pIter; xml_private_t *p = pIter->_private; pIter = pIter->next; if(is_set(p->flags, xpf_created)) { char *name = strdup((const char *)prop->name); char *value = crm_element_value_copy(new_xml, name); crm_trace("Created %s@%s=%s", new_xml->name, name, value); /* Remove plus create won't work as it will modify the relative attribute ordering */ if (pcmk__check_acl(new_xml, name, xpf_acl_write)) { pcmk__mark_xml_attr_dirty(prop); } else { xmlUnsetProp(new_xml, prop->name); /* Remove - change not allowed */ } free(value); free(name); } } for (cIter = __xml_first_child(old_xml); cIter != NULL; ) { xmlNode *old_child = cIter; xmlNode *new_child = find_element(new_xml, cIter, TRUE); cIter = __xml_next(cIter); if(new_child) { - __xml_diff_object(old_child, new_child); + __xml_diff_object(old_child, new_child, TRUE); } else { /* Create then free (which will check the acls if necessary) */ xmlNode *candidate = add_node_copy(new_xml, old_child); xmlNode *top = xmlDocGetRootElement(candidate->doc); __xml_node_clean(candidate); pcmk__apply_acl(top); /* Make sure any ACLs are applied to 'candidate' */ /* Record the old position */ free_xml_with_position(candidate, __xml_offset(old_child)); if (find_element(new_xml, old_child, TRUE) == NULL) { xml_private_t *p = old_child->_private; p->flags |= xpf_skip; } } } for (cIter = __xml_first_child(new_xml); cIter != NULL; ) { xmlNode *new_child = cIter; xmlNode *old_child = find_element(old_xml, cIter, TRUE); cIter = __xml_next(cIter); if(old_child == NULL) { xml_private_t *p = new_child->_private; p->flags |= xpf_skip; - __xml_diff_object(old_child, new_child); + __xml_diff_object(old_child, new_child, TRUE); } else { /* Check for movement, we already checked for differences */ int p_new = __xml_offset(new_child); int p_old = __xml_offset(old_child); if(p_old != p_new) { xml_private_t *p = new_child->_private; crm_info("%s.%s moved from %d to %d", new_child->name, ID(new_child), p_old, p_new); __xml_node_dirty(new_xml); p->flags |= xpf_moved; if(p_old > p_new) { p = old_child->_private; } else { p = new_child->_private; } p->flags |= xpf_skip; } } } } void xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml) { pcmk__set_xml_flag(new_xml, xpf_lazy); xml_calculate_changes(old_xml, new_xml); } void xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml) { CRM_CHECK(safe_str_eq(crm_element_name(old_xml), crm_element_name(new_xml)), return); CRM_CHECK(safe_str_eq(ID(old_xml), ID(new_xml)), return); if(xml_tracking_changes(new_xml) == FALSE) { xml_track_changes(new_xml, NULL, NULL, FALSE); } - __xml_diff_object(old_xml, new_xml); + __xml_diff_object(old_xml, new_xml, FALSE); } xmlNode * diff_xml_object(xmlNode * old, xmlNode * new, gboolean suppress) { xmlNode *tmp1 = NULL; xmlNode *diff = create_xml_node(NULL, "diff"); xmlNode *removed = create_xml_node(diff, "diff-removed"); xmlNode *added = create_xml_node(diff, "diff-added"); crm_xml_add(diff, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET); tmp1 = subtract_xml_object(removed, old, new, FALSE, NULL, "removed:top"); if (suppress && tmp1 != NULL && can_prune_leaf(tmp1)) { free_xml(tmp1); } tmp1 = subtract_xml_object(added, new, old, TRUE, NULL, "added:top"); if (suppress && tmp1 != NULL && can_prune_leaf(tmp1)) { free_xml(tmp1); } if (added->children == NULL && removed->children == NULL) { free_xml(diff); diff = NULL; } return diff; } gboolean can_prune_leaf(xmlNode * xml_node) { xmlNode *cIter = NULL; xmlAttrPtr pIter = NULL; gboolean can_prune = TRUE; const char *name = crm_element_name(xml_node); if (safe_str_eq(name, XML_TAG_RESOURCE_REF) || safe_str_eq(name, XML_CIB_TAG_OBJ_REF) || safe_str_eq(name, XML_ACL_TAG_ROLE_REF) || safe_str_eq(name, XML_ACL_TAG_ROLE_REFv1)) { return FALSE; } for (pIter = pcmk__first_xml_attr(xml_node); pIter != NULL; pIter = pIter->next) { const char *p_name = (const char *)pIter->name; if (strcmp(p_name, XML_ATTR_ID) == 0) { continue; } can_prune = FALSE; } cIter = __xml_first_child(xml_node); while (cIter) { xmlNode *child = cIter; cIter = __xml_next(cIter); if (can_prune_leaf(child)) { free_xml(child); } else { can_prune = FALSE; } } return can_prune; } static xmlNode * find_xml_comment(xmlNode * root, xmlNode * search_comment, gboolean exact) { xmlNode *a_child = NULL; int search_offset = __xml_offset(search_comment); CRM_CHECK(search_comment->type == XML_COMMENT_NODE, return NULL); for (a_child = __xml_first_child(root); a_child != NULL; a_child = __xml_next(a_child)) { if (exact) { int offset = __xml_offset(a_child); xml_private_t *p = a_child->_private; if (offset < search_offset) { continue; } else if (offset > search_offset) { return NULL; } if (is_set(p->flags, xpf_skip)) { continue; } } if (a_child->type == XML_COMMENT_NODE && safe_str_eq((const char *)a_child->content, (const char *)search_comment->content)) { return a_child; } else if (exact) { return NULL; } } return NULL; } static xmlNode * subtract_xml_comment(xmlNode * parent, xmlNode * left, xmlNode * right, gboolean * changed) { CRM_CHECK(left != NULL, return NULL); CRM_CHECK(left->type == XML_COMMENT_NODE, return NULL); if (right == NULL || safe_str_neq((const char *)left->content, (const char *)right->content)) { xmlNode *deleted = NULL; deleted = add_node_copy(parent, left); *changed = TRUE; return deleted; } return NULL; } xmlNode * subtract_xml_object(xmlNode * parent, xmlNode * left, xmlNode * right, gboolean full, gboolean * changed, const char *marker) { gboolean dummy = FALSE; gboolean skip = FALSE; xmlNode *diff = NULL; xmlNode *right_child = NULL; xmlNode *left_child = NULL; xmlAttrPtr xIter = NULL; const char *id = NULL; const char *name = NULL; const char *value = NULL; const char *right_val = NULL; int lpc = 0; static int filter_len = DIMOF(filter); if (changed == NULL) { changed = &dummy; } if (left == NULL) { return NULL; } if (left->type == XML_COMMENT_NODE) { return subtract_xml_comment(parent, left, right, changed); } id = ID(left); if (right == NULL) { xmlNode *deleted = NULL; crm_trace("Processing <%s id=%s> (complete copy)", crm_element_name(left), id); deleted = add_node_copy(parent, left); crm_xml_add(deleted, XML_DIFF_MARKER, marker); *changed = TRUE; return deleted; } name = crm_element_name(left); CRM_CHECK(name != NULL, return NULL); CRM_CHECK(safe_str_eq(crm_element_name(left), crm_element_name(right)), return NULL); /* check for XML_DIFF_MARKER in a child */ value = crm_element_value(right, XML_DIFF_MARKER); if (value != NULL && strcmp(value, "removed:top") == 0) { crm_trace("We are the root of the deletion: %s.id=%s", name, id); *changed = TRUE; return NULL; } /* Avoiding creating the full heirarchy would save even more work here */ diff = create_xml_node(parent, name); /* Reset filter */ for (lpc = 0; lpc < filter_len; lpc++) { filter[lpc].found = FALSE; } /* changes to child objects */ for (left_child = __xml_first_child(left); left_child != NULL; left_child = __xml_next(left_child)) { gboolean child_changed = FALSE; right_child = find_element(right, left_child, FALSE); subtract_xml_object(diff, left_child, right_child, full, &child_changed, marker); if (child_changed) { *changed = TRUE; } } if (*changed == FALSE) { /* Nothing to do */ } else if (full) { xmlAttrPtr pIter = NULL; for (pIter = pcmk__first_xml_attr(left); pIter != NULL; pIter = pIter->next) { const char *p_name = (const char *)pIter->name; const char *p_value = pcmk__xml_attr_value(pIter); xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value); } /* We already have everything we need... */ goto done; } /* changes to name/value pairs */ for (xIter = pcmk__first_xml_attr(left); xIter != NULL; xIter = xIter->next) { const char *prop_name = (const char *)xIter->name; xmlAttrPtr right_attr = NULL; xml_private_t *p = NULL; if (strcmp(prop_name, XML_ATTR_ID) == 0) { /* id already obtained when present ~ this case, so just reuse */ xmlSetProp(diff, (pcmkXmlStr) XML_ATTR_ID, (pcmkXmlStr) id); continue; } skip = FALSE; for (lpc = 0; skip == FALSE && lpc < filter_len; lpc++) { if (filter[lpc].found == FALSE && strcmp(prop_name, filter[lpc].string) == 0) { filter[lpc].found = TRUE; skip = TRUE; break; } } if (skip) { continue; } right_attr = xmlHasProp(right, (pcmkXmlStr) prop_name); if (right_attr) { p = right_attr->_private; } right_val = crm_element_value(right, prop_name); if (right_val == NULL || (p && is_set(p->flags, xpf_deleted))) { /* new */ *changed = TRUE; if (full) { xmlAttrPtr pIter = NULL; for (pIter = pcmk__first_xml_attr(left); pIter != NULL; pIter = pIter->next) { const char *p_name = (const char *)pIter->name; const char *p_value = pcmk__xml_attr_value(pIter); xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value); } break; } else { const char *left_value = crm_element_value(left, prop_name); xmlSetProp(diff, (pcmkXmlStr) prop_name, (pcmkXmlStr) value); crm_xml_add(diff, prop_name, left_value); } } else { /* Only now do we need the left value */ const char *left_value = crm_element_value(left, prop_name); if (strcmp(left_value, right_val) == 0) { /* unchanged */ } else { *changed = TRUE; if (full) { xmlAttrPtr pIter = NULL; crm_trace("Changes detected to %s in <%s id=%s>", prop_name, crm_element_name(left), id); for (pIter = pcmk__first_xml_attr(left); pIter != NULL; pIter = pIter->next) { const char *p_name = (const char *)pIter->name; const char *p_value = pcmk__xml_attr_value(pIter); xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value); } break; } else { crm_trace("Changes detected to %s (%s -> %s) in <%s id=%s>", prop_name, left_value, right_val, crm_element_name(left), id); crm_xml_add(diff, prop_name, left_value); } } } } if (*changed == FALSE) { free_xml(diff); return NULL; } else if (full == FALSE && id) { crm_xml_add(diff, XML_ATTR_ID, id); } done: return diff; } static int add_xml_comment(xmlNode * parent, xmlNode * target, xmlNode * update) { CRM_CHECK(update != NULL, return 0); CRM_CHECK(update->type == XML_COMMENT_NODE, return 0); if (target == NULL) { target = find_xml_comment(parent, update, FALSE); } if (target == NULL) { add_node_copy(parent, update); /* We won't reach here currently */ } else if (safe_str_neq((const char *)target->content, (const char *)update->content)) { xmlFree(target->content); target->content = xmlStrdup(update->content); } return 0; } static int add_xml_object(xmlNode * parent, xmlNode * target, xmlNode * update, gboolean as_diff) { xmlNode *a_child = NULL; const char *object_name = NULL, *object_href = NULL, *object_href_val = NULL; #if XML_PARSE_DEBUG crm_log_xml_trace("update:", update); crm_log_xml_trace("target:", target); #endif CRM_CHECK(update != NULL, return 0); if (update->type == XML_COMMENT_NODE) { return add_xml_comment(parent, target, update); } object_name = crm_element_name(update); object_href_val = ID(update); if (object_href_val != NULL) { object_href = XML_ATTR_ID; } else { object_href_val = crm_element_value(update, XML_ATTR_IDREF); object_href = (object_href_val == NULL) ? NULL : XML_ATTR_IDREF; } CRM_CHECK(object_name != NULL, return 0); CRM_CHECK(target != NULL || parent != NULL, return 0); if (target == NULL) { target = find_entity_by_attr_or_just_name(parent, object_name, object_href, object_href_val); } if (target == NULL) { target = create_xml_node(parent, object_name); CRM_CHECK(target != NULL, return 0); #if XML_PARSER_DEBUG crm_trace("Added <%s%s%s%s%s/>", crm_str(object_name), object_href ? " " : "", object_href ? object_href : "", object_href ? "=" : "", object_href ? object_href_val : ""); } else { crm_trace("Found node <%s%s%s%s%s/> to update", crm_str(object_name), object_href ? " " : "", object_href ? object_href : "", object_href ? "=" : "", object_href ? object_href_val : ""); #endif } CRM_CHECK(safe_str_eq(crm_element_name(target), crm_element_name(update)), return 0); if (as_diff == FALSE) { /* So that expand_plus_plus() gets called */ copy_in_properties(target, update); } else { /* No need for expand_plus_plus(), just raw speed */ xmlAttrPtr pIter = NULL; for (pIter = pcmk__first_xml_attr(update); pIter != NULL; pIter = pIter->next) { const char *p_name = (const char *)pIter->name; const char *p_value = pcmk__xml_attr_value(pIter); /* Remove it first so the ordering of the update is preserved */ xmlUnsetProp(target, (pcmkXmlStr) p_name); xmlSetProp(target, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value); } } for (a_child = __xml_first_child(update); a_child != NULL; a_child = __xml_next(a_child)) { #if XML_PARSER_DEBUG crm_trace("Updating child <%s%s%s%s%s/>", crm_str(object_name), object_href ? " " : "", object_href ? object_href : "", object_href ? "=" : "", object_href ? object_href_val : ""); #endif add_xml_object(target, NULL, a_child, as_diff); } #if XML_PARSER_DEBUG crm_trace("Finished with <%s%s%s%s%s/>", crm_str(object_name), object_href ? " " : "", object_href ? object_href : "", object_href ? "=" : "", object_href ? object_href_val : ""); #endif return 0; } gboolean update_xml_child(xmlNode * child, xmlNode * to_update) { gboolean can_update = TRUE; xmlNode *child_of_child = NULL; CRM_CHECK(child != NULL, return FALSE); CRM_CHECK(to_update != NULL, return FALSE); if (safe_str_neq(crm_element_name(to_update), crm_element_name(child))) { can_update = FALSE; } else if (safe_str_neq(ID(to_update), ID(child))) { can_update = FALSE; } else if (can_update) { #if XML_PARSER_DEBUG crm_log_xml_trace(child, "Update match found..."); #endif add_xml_object(NULL, child, to_update, FALSE); } for (child_of_child = __xml_first_child(child); child_of_child != NULL; child_of_child = __xml_next(child_of_child)) { /* only update the first one */ if (can_update) { break; } can_update = update_xml_child(child_of_child, to_update); } return can_update; } int find_xml_children(xmlNode ** children, xmlNode * root, const char *tag, const char *field, const char *value, gboolean search_matches) { int match_found = 0; CRM_CHECK(root != NULL, return FALSE); CRM_CHECK(children != NULL, return FALSE); if (tag != NULL && safe_str_neq(tag, crm_element_name(root))) { } else if (value != NULL && safe_str_neq(value, crm_element_value(root, field))) { } else { if (*children == NULL) { *children = create_xml_node(NULL, __FUNCTION__); } add_node_copy(*children, root); match_found = 1; } if (search_matches || match_found == 0) { xmlNode *child = NULL; for (child = __xml_first_child(root); child != NULL; child = __xml_next(child)) { match_found += find_xml_children(children, child, tag, field, value, search_matches); } } return match_found; } gboolean replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only) { gboolean can_delete = FALSE; xmlNode *child_of_child = NULL; const char *up_id = NULL; const char *child_id = NULL; const char *right_val = NULL; CRM_CHECK(child != NULL, return FALSE); CRM_CHECK(update != NULL, return FALSE); up_id = ID(update); child_id = ID(child); if (up_id == NULL || (child_id && strcmp(child_id, up_id) == 0)) { can_delete = TRUE; } if (safe_str_neq(crm_element_name(update), crm_element_name(child))) { can_delete = FALSE; } if (can_delete && delete_only) { xmlAttrPtr pIter = NULL; for (pIter = pcmk__first_xml_attr(update); pIter != NULL; pIter = pIter->next) { const char *p_name = (const char *)pIter->name; const char *p_value = pcmk__xml_attr_value(pIter); right_val = crm_element_value(child, p_name); if (safe_str_neq(p_value, right_val)) { can_delete = FALSE; } } } if (can_delete && parent != NULL) { crm_log_xml_trace(child, "Delete match found..."); if (delete_only || update == NULL) { free_xml(child); } else { xmlNode *tmp = copy_xml(update); xmlDoc *doc = tmp->doc; xmlNode *old = NULL; xml_accept_changes(tmp); old = xmlReplaceNode(child, tmp); if(xml_tracking_changes(tmp)) { /* Replaced sections may have included relevant ACLs */ pcmk__apply_acl(tmp); } xml_calculate_changes(old, tmp); xmlDocSetRootElement(doc, old); free_xml(old); } child = NULL; return TRUE; } else if (can_delete) { crm_log_xml_debug(child, "Cannot delete the search root"); can_delete = FALSE; } child_of_child = __xml_first_child(child); while (child_of_child) { xmlNode *next = __xml_next(child_of_child); can_delete = replace_xml_child(child, child_of_child, update, delete_only); /* only delete the first one */ if (can_delete) { child_of_child = NULL; } else { child_of_child = next; } } return can_delete; } xmlNode * sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive) { xmlNode *child = NULL; GSList *nvpairs = NULL; xmlNode *result = NULL; const char *name = NULL; CRM_CHECK(input != NULL, return NULL); name = crm_element_name(input); CRM_CHECK(name != NULL, return NULL); result = create_xml_node(parent, name); nvpairs = pcmk_xml_attrs2nvpairs(input); nvpairs = pcmk_sort_nvpairs(nvpairs); pcmk_nvpairs2xml_attrs(nvpairs, result); pcmk_free_nvpairs(nvpairs); for (child = __xml_first_child(input); child != NULL; child = __xml_next(child)) { if (recursive) { sorted_xml(child, result, recursive); } else { add_node_copy(result, child); } } return result; } xmlNode * first_named_child(const xmlNode *parent, const char *name) { xmlNode *match = NULL; for (match = __xml_first_child(parent); match != NULL; match = __xml_next(match)) { /* * name == NULL gives first child regardless of name; this is * semantically incorrect in this function, but may be necessary * due to prior use of xml_child_iter_filter */ if (name == NULL || strcmp((const char *)match->name, name) == 0) { return match; } } return NULL; } /*! * \brief Get next instance of same XML tag * * \param[in] sibling XML tag to start from * * \return Next sibling XML tag with same name */ xmlNode * crm_next_same_xml(const xmlNode *sibling) { xmlNode *match = __xml_next(sibling); const char *name = crm_element_name(sibling); while (match != NULL) { if (!strcmp(crm_element_name(match), name)) { return match; } match = __xml_next(match); } return NULL; } void crm_xml_init(void) { static bool init = TRUE; if(init) { init = FALSE; /* The default allocator XML_BUFFER_ALLOC_EXACT does far too many * realloc_safe()s and it can take upwards of 18 seconds (yes, seconds) * to dump a 28kb tree which XML_BUFFER_ALLOC_DOUBLEIT can do in * less than 1 second. */ xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT); /* Populate and free the _private field when nodes are created and destroyed */ xmlDeregisterNodeDefault(pcmkDeregisterNode); xmlRegisterNodeDefault(pcmkRegisterNode); crm_schema_init(); } } void crm_xml_cleanup(void) { crm_info("Cleaning up memory from libxml2"); crm_schema_cleanup(); xmlCleanupParser(); } #define XPATH_MAX 512 xmlNode * expand_idref(xmlNode * input, xmlNode * top) { const char *tag = NULL; const char *ref = NULL; xmlNode *result = input; if (result == NULL) { return NULL; } else if (top == NULL) { top = input; } tag = crm_element_name(result); ref = crm_element_value(result, XML_ATTR_IDREF); if (ref != NULL) { char *xpath_string = crm_strdup_printf("//%s[@id='%s']", tag, ref); result = get_xpath_object(xpath_string, top, LOG_ERR); if (result == NULL) { char *nodePath = (char *)xmlGetNodePath(top); crm_err("No match for %s found in %s: Invalid configuration", xpath_string, crm_str(nodePath)); free(nodePath); } free(xpath_string); } return result; } void crm_destroy_xml(gpointer data) { free_xml(data); }