diff --git a/cts/cli/regression.crm_resource.exp b/cts/cli/regression.crm_resource.exp index 3b189aecf5..d9e473b21c 100644 --- a/cts/cli/regression.crm_resource.exp +++ b/cts/cli/regression.crm_resource.exp @@ -1,4049 +1,4049 @@ =#=#=#= Begin test: crm_resource run with extra arguments =#=#=#= crm_resource: non-option ARGV-elements: [1 of 2] foo [2 of 2] bar =#=#=#= End test: crm_resource run with extra arguments - Incorrect usage (64) =#=#=#= * Passed: crm_resource - crm_resource run with extra arguments =#=#=#= Begin test: List all available resource options (invalid type) =#=#=#= crm_resource: Error parsing option --list-options =#=#=#= End test: List all available resource options (invalid type) - Incorrect usage (64) =#=#=#= * Passed: crm_resource - List all available resource options (invalid type) =#=#=#= Begin test: List all available resource options (invalid type) =#=#=#= crm_resource: Error parsing option --list-options =#=#=#= End test: List all available resource options (invalid type) - Incorrect usage (64) =#=#=#= * Passed: crm_resource - List all available resource options (invalid type) =#=#=#= Begin test: List non-advanced primitive meta-attributes =#=#=#= Primitive meta-attributes Meta-attributes applicable to primitive resources * priority: Resource assignment priority * If not all resources can be active, the cluster will stop lower-priority resources in order to keep higher-priority ones active. * Possible values: score (default: ) * critical: Default value for influence in colocation constraints * Use this value as the default for influence in all colocation constraints involving this resource, as well as in the implicit colocation constraints created if this resource is in a group. * Possible values: boolean (default: ) * target-role: State the cluster should attempt to keep this resource in * "Stopped" forces the resource to be stopped. "Started" allows the resource to be started (and in the case of promotable clone resources, promoted if appropriate). "Unpromoted" allows the resource to be started, but only in the unpromoted role if the resource is promotable. "Promoted" is equivalent to "Started". * Possible values: "Stopped", "Started" (default), "Unpromoted", "Promoted" * is-managed: Whether the cluster is allowed to actively change the resource's state * If false, the cluster will not start, stop, promote, or demote the resource on any node. Recurring actions for the resource are unaffected. If true, a true value for the maintenance-mode cluster option, the maintenance node attribute, or the maintenance resource meta-attribute overrides this. * Possible values: boolean (default: ) * maintenance: If true, the cluster will not schedule any actions involving the resource * If true, the cluster will not start, stop, promote, or demote the resource on any node, and will pause any recurring monitors (except those specifying role as "Stopped"). If false, a true value for the maintenance-mode cluster option or maintenance node attribute overrides this. * Possible values: boolean (default: ) * resource-stickiness: Score to add to the current node when a resource is already active * Score to add to the current node when a resource is already active. This allows running resources to stay where they are, even if they would be placed elsewhere if they were being started from a stopped state. The default is 1 for individual clone instances, and 0 for all other resources. * Possible values: score (no default) * requires: Conditions under which the resource can be started * Conditions under which the resource can be started. "nothing" means the cluster can always start this resource. "quorum" means the cluster can start this resource only if a majority of the configured nodes are active. "fencing" means the cluster can start this resource only if a majority of the configured nodes are active and any failed or unknown nodes have been fenced. "unfencing" means the cluster can start this resource only if a majority of the configured nodes are active and any failed or unknown nodes have been fenced, and only on nodes that have been unfenced. The default is "quorum" for resources with a class of stonith; otherwise, "unfencing" if unfencing is active in the cluster; otherwise, "fencing" if the stonith-enabled cluster option is true; otherwise, "quorum". * Possible values: "nothing", "quorum", "fencing", "unfencing" * migration-threshold: Number of failures on a node before the resource becomes ineligible to run there. * Number of failures that may occur for this resource on a node, before that node is marked ineligible to host this resource. A value of 0 indicates that this feature is disabled (the node will never be marked ineligible). By contrast, the cluster treats "INFINITY" (the default) as a very large but finite number. This option has an effect only if the failed operation specifies its on-fail attribute as "restart" (the default), and additionally for failed start operations, if the start-failure-is-fatal cluster property is set to false. * Possible values: score (default: ) * failure-timeout: Number of seconds before acting as if a failure had not occurred * Number of seconds after a failed action for this resource before acting as if the failure had not occurred, and potentially allowing the resource back to the node on which it failed. A value of 0 indicates that this feature is disabled. * Possible values: duration (default: ) * multiple-active: What to do if the cluster finds the resource active on more than one node * What to do if the cluster finds the resource active on more than one node. "block" means to mark the resource as unmanaged. "stop_only" means to stop all active instances of this resource and leave them stopped. "stop_start" means to stop all active instances of this resource and start the resource in one location only. "stop_unexpected" means to stop all active instances of this resource except where the resource should be active. (This should be used only when extra instances are not expected to disrupt existing instances, and the resource agent's monitor of an existing instance is capable of detecting any problems that could be caused. Note that any resources ordered after this one will still need to be restarted.) * Possible values: "block", "stop_only", "stop_start" (default), "stop_unexpected" * allow-migrate: Whether the cluster should try to "live migrate" this resource when it needs to be moved * Whether the cluster should try to "live migrate" this resource when it needs to be moved. The default is true for ocf:pacemaker:remote resources, and false otherwise. * Possible values: boolean (no default) * allow-unhealthy-nodes: Whether the resource should be allowed to run on a node even if the node's health score would otherwise prevent it * Possible values: boolean (default: ) * container-attribute-target: Where to check user-defined node attributes * Whether to check user-defined node attributes on the physical host where a container is running or on the local node. This is usually set for a bundle resource and inherited by the bundle's primitive resource. A value of "host" means to check user-defined node attributes on the underlying physical host. Any other value means to check user-defined node attributes on the local node (for a bundled primitive resource, this is the bundle node). * Possible values: string (no default) * remote-node: Name of the Pacemaker Remote guest node this resource is associated with, if any * Name of the Pacemaker Remote guest node this resource is associated with, if any. If specified, this both enables the resource as a guest node and defines the unique name used to identify the guest node. The guest must be configured to run the Pacemaker Remote daemon when it is started. WARNING: This value cannot overlap with any resource or node IDs. * Possible values: string (no default) * remote-addr: If remote-node is specified, the IP address or hostname used to connect to the guest via Pacemaker Remote * If remote-node is specified, the IP address or hostname used to connect to the guest via Pacemaker Remote. The Pacemaker Remote daemon on the guest must be configured to accept connections on this address. The default is the value of the remote-node meta-attribute. * Possible values: string (no default) * remote-port: If remote-node is specified, port on the guest used for its Pacemaker Remote connection * If remote-node is specified, the port on the guest used for its Pacemaker Remote connection. The Pacemaker Remote daemon on the guest must be configured to listen on this port. * Possible values: port (default: ) * remote-connect-timeout: If remote-node is specified, how long before a pending Pacemaker Remote guest connection times out. * Possible values: timeout (default: ) * remote-allow-migrate: If remote-node is specified, this acts as the allow-migrate meta-attribute for the implicit remote connection resource (ocf:pacemaker:remote). * Possible values: boolean (default: ) =#=#=#= End test: List non-advanced primitive meta-attributes - OK (0) =#=#=#= * Passed: crm_resource - List non-advanced primitive meta-attributes =#=#=#= Begin test: List non-advanced primitive meta-attributes (XML) =#=#=#= 1.1 Meta-attributes applicable to primitive resources Primitive meta-attributes If not all resources can be active, the cluster will stop lower-priority resources in order to keep higher-priority ones active. Resource assignment priority Use this value as the default for influence in all colocation constraints involving this resource, as well as in the implicit colocation constraints created if this resource is in a group. Default value for influence in colocation constraints "Stopped" forces the resource to be stopped. "Started" allows the resource to be started (and in the case of promotable clone resources, promoted if appropriate). "Unpromoted" allows the resource to be started, but only in the unpromoted role if the resource is promotable. "Promoted" is equivalent to "Started". State the cluster should attempt to keep this resource in If false, the cluster will not start, stop, promote, or demote the resource on any node. Recurring actions for the resource are unaffected. If true, a true value for the maintenance-mode cluster option, the maintenance node attribute, or the maintenance resource meta-attribute overrides this. Whether the cluster is allowed to actively change the resource's state If true, the cluster will not start, stop, promote, or demote the resource on any node, and will pause any recurring monitors (except those specifying role as "Stopped"). If false, a true value for the maintenance-mode cluster option or maintenance node attribute overrides this. If true, the cluster will not schedule any actions involving the resource Score to add to the current node when a resource is already active. This allows running resources to stay where they are, even if they would be placed elsewhere if they were being started from a stopped state. The default is 1 for individual clone instances, and 0 for all other resources. Score to add to the current node when a resource is already active Conditions under which the resource can be started. "nothing" means the cluster can always start this resource. "quorum" means the cluster can start this resource only if a majority of the configured nodes are active. "fencing" means the cluster can start this resource only if a majority of the configured nodes are active and any failed or unknown nodes have been fenced. "unfencing" means the cluster can start this resource only if a majority of the configured nodes are active and any failed or unknown nodes have been fenced, and only on nodes that have been unfenced. The default is "quorum" for resources with a class of stonith; otherwise, "unfencing" if unfencing is active in the cluster; otherwise, "fencing" if the stonith-enabled cluster option is true; otherwise, "quorum". Conditions under which the resource can be started Number of failures that may occur for this resource on a node, before that node is marked ineligible to host this resource. A value of 0 indicates that this feature is disabled (the node will never be marked ineligible). By contrast, the cluster treats "INFINITY" (the default) as a very large but finite number. This option has an effect only if the failed operation specifies its on-fail attribute as "restart" (the default), and additionally for failed start operations, if the start-failure-is-fatal cluster property is set to false. Number of failures on a node before the resource becomes ineligible to run there. Number of seconds after a failed action for this resource before acting as if the failure had not occurred, and potentially allowing the resource back to the node on which it failed. A value of 0 indicates that this feature is disabled. Number of seconds before acting as if a failure had not occurred What to do if the cluster finds the resource active on more than one node. "block" means to mark the resource as unmanaged. "stop_only" means to stop all active instances of this resource and leave them stopped. "stop_start" means to stop all active instances of this resource and start the resource in one location only. "stop_unexpected" means to stop all active instances of this resource except where the resource should be active. (This should be used only when extra instances are not expected to disrupt existing instances, and the resource agent's monitor of an existing instance is capable of detecting any problems that could be caused. Note that any resources ordered after this one will still need to be restarted.) What to do if the cluster finds the resource active on more than one node Whether the cluster should try to "live migrate" this resource when it needs to be moved. The default is true for ocf:pacemaker:remote resources, and false otherwise. Whether the cluster should try to "live migrate" this resource when it needs to be moved Whether the resource should be allowed to run on a node even if the node's health score would otherwise prevent it Whether the resource should be allowed to run on a node even if the node's health score would otherwise prevent it Whether to check user-defined node attributes on the physical host where a container is running or on the local node. This is usually set for a bundle resource and inherited by the bundle's primitive resource. A value of "host" means to check user-defined node attributes on the underlying physical host. Any other value means to check user-defined node attributes on the local node (for a bundled primitive resource, this is the bundle node). Where to check user-defined node attributes Name of the Pacemaker Remote guest node this resource is associated with, if any. If specified, this both enables the resource as a guest node and defines the unique name used to identify the guest node. The guest must be configured to run the Pacemaker Remote daemon when it is started. WARNING: This value cannot overlap with any resource or node IDs. Name of the Pacemaker Remote guest node this resource is associated with, if any If remote-node is specified, the IP address or hostname used to connect to the guest via Pacemaker Remote. The Pacemaker Remote daemon on the guest must be configured to accept connections on this address. The default is the value of the remote-node meta-attribute. If remote-node is specified, the IP address or hostname used to connect to the guest via Pacemaker Remote If remote-node is specified, the port on the guest used for its Pacemaker Remote connection. The Pacemaker Remote daemon on the guest must be configured to listen on this port. If remote-node is specified, port on the guest used for its Pacemaker Remote connection If remote-node is specified, how long before a pending Pacemaker Remote guest connection times out. If remote-node is specified, how long before a pending Pacemaker Remote guest connection times out. If remote-node is specified, this acts as the allow-migrate meta-attribute for the implicit remote connection resource (ocf:pacemaker:remote). If remote-node is specified, this acts as the allow-migrate meta-attribute for the implicit remote connection resource (ocf:pacemaker:remote). =#=#=#= End test: List non-advanced primitive meta-attributes (XML) - OK (0) =#=#=#= * Passed: crm_resource - List non-advanced primitive meta-attributes (XML) =#=#=#= Begin test: List all available primitive meta-attributes =#=#=#= Primitive meta-attributes Meta-attributes applicable to primitive resources * priority: Resource assignment priority * If not all resources can be active, the cluster will stop lower-priority resources in order to keep higher-priority ones active. * Possible values: score (default: ) * critical: Default value for influence in colocation constraints * Use this value as the default for influence in all colocation constraints involving this resource, as well as in the implicit colocation constraints created if this resource is in a group. * Possible values: boolean (default: ) * target-role: State the cluster should attempt to keep this resource in * "Stopped" forces the resource to be stopped. "Started" allows the resource to be started (and in the case of promotable clone resources, promoted if appropriate). "Unpromoted" allows the resource to be started, but only in the unpromoted role if the resource is promotable. "Promoted" is equivalent to "Started". * Possible values: "Stopped", "Started" (default), "Unpromoted", "Promoted" * is-managed: Whether the cluster is allowed to actively change the resource's state * If false, the cluster will not start, stop, promote, or demote the resource on any node. Recurring actions for the resource are unaffected. If true, a true value for the maintenance-mode cluster option, the maintenance node attribute, or the maintenance resource meta-attribute overrides this. * Possible values: boolean (default: ) * maintenance: If true, the cluster will not schedule any actions involving the resource * If true, the cluster will not start, stop, promote, or demote the resource on any node, and will pause any recurring monitors (except those specifying role as "Stopped"). If false, a true value for the maintenance-mode cluster option or maintenance node attribute overrides this. * Possible values: boolean (default: ) * resource-stickiness: Score to add to the current node when a resource is already active * Score to add to the current node when a resource is already active. This allows running resources to stay where they are, even if they would be placed elsewhere if they were being started from a stopped state. The default is 1 for individual clone instances, and 0 for all other resources. * Possible values: score (no default) * requires: Conditions under which the resource can be started * Conditions under which the resource can be started. "nothing" means the cluster can always start this resource. "quorum" means the cluster can start this resource only if a majority of the configured nodes are active. "fencing" means the cluster can start this resource only if a majority of the configured nodes are active and any failed or unknown nodes have been fenced. "unfencing" means the cluster can start this resource only if a majority of the configured nodes are active and any failed or unknown nodes have been fenced, and only on nodes that have been unfenced. The default is "quorum" for resources with a class of stonith; otherwise, "unfencing" if unfencing is active in the cluster; otherwise, "fencing" if the stonith-enabled cluster option is true; otherwise, "quorum". * Possible values: "nothing", "quorum", "fencing", "unfencing" * migration-threshold: Number of failures on a node before the resource becomes ineligible to run there. * Number of failures that may occur for this resource on a node, before that node is marked ineligible to host this resource. A value of 0 indicates that this feature is disabled (the node will never be marked ineligible). By contrast, the cluster treats "INFINITY" (the default) as a very large but finite number. This option has an effect only if the failed operation specifies its on-fail attribute as "restart" (the default), and additionally for failed start operations, if the start-failure-is-fatal cluster property is set to false. * Possible values: score (default: ) * failure-timeout: Number of seconds before acting as if a failure had not occurred * Number of seconds after a failed action for this resource before acting as if the failure had not occurred, and potentially allowing the resource back to the node on which it failed. A value of 0 indicates that this feature is disabled. * Possible values: duration (default: ) * multiple-active: What to do if the cluster finds the resource active on more than one node * What to do if the cluster finds the resource active on more than one node. "block" means to mark the resource as unmanaged. "stop_only" means to stop all active instances of this resource and leave them stopped. "stop_start" means to stop all active instances of this resource and start the resource in one location only. "stop_unexpected" means to stop all active instances of this resource except where the resource should be active. (This should be used only when extra instances are not expected to disrupt existing instances, and the resource agent's monitor of an existing instance is capable of detecting any problems that could be caused. Note that any resources ordered after this one will still need to be restarted.) * Possible values: "block", "stop_only", "stop_start" (default), "stop_unexpected" * allow-migrate: Whether the cluster should try to "live migrate" this resource when it needs to be moved * Whether the cluster should try to "live migrate" this resource when it needs to be moved. The default is true for ocf:pacemaker:remote resources, and false otherwise. * Possible values: boolean (no default) * allow-unhealthy-nodes: Whether the resource should be allowed to run on a node even if the node's health score would otherwise prevent it * Possible values: boolean (default: ) * container-attribute-target: Where to check user-defined node attributes * Whether to check user-defined node attributes on the physical host where a container is running or on the local node. This is usually set for a bundle resource and inherited by the bundle's primitive resource. A value of "host" means to check user-defined node attributes on the underlying physical host. Any other value means to check user-defined node attributes on the local node (for a bundled primitive resource, this is the bundle node). * Possible values: string (no default) * remote-node: Name of the Pacemaker Remote guest node this resource is associated with, if any * Name of the Pacemaker Remote guest node this resource is associated with, if any. If specified, this both enables the resource as a guest node and defines the unique name used to identify the guest node. The guest must be configured to run the Pacemaker Remote daemon when it is started. WARNING: This value cannot overlap with any resource or node IDs. * Possible values: string (no default) * remote-addr: If remote-node is specified, the IP address or hostname used to connect to the guest via Pacemaker Remote * If remote-node is specified, the IP address or hostname used to connect to the guest via Pacemaker Remote. The Pacemaker Remote daemon on the guest must be configured to accept connections on this address. The default is the value of the remote-node meta-attribute. * Possible values: string (no default) * remote-port: If remote-node is specified, port on the guest used for its Pacemaker Remote connection * If remote-node is specified, the port on the guest used for its Pacemaker Remote connection. The Pacemaker Remote daemon on the guest must be configured to listen on this port. * Possible values: port (default: ) * remote-connect-timeout: If remote-node is specified, how long before a pending Pacemaker Remote guest connection times out. * Possible values: timeout (default: ) * remote-allow-migrate: If remote-node is specified, this acts as the allow-migrate meta-attribute for the implicit remote connection resource (ocf:pacemaker:remote). * Possible values: boolean (default: ) =#=#=#= End test: List all available primitive meta-attributes - OK (0) =#=#=#= * Passed: crm_resource - List all available primitive meta-attributes =#=#=#= Begin test: List all available primitive meta-attributes (XML) =#=#=#= 1.1 Meta-attributes applicable to primitive resources Primitive meta-attributes If not all resources can be active, the cluster will stop lower-priority resources in order to keep higher-priority ones active. Resource assignment priority Use this value as the default for influence in all colocation constraints involving this resource, as well as in the implicit colocation constraints created if this resource is in a group. Default value for influence in colocation constraints "Stopped" forces the resource to be stopped. "Started" allows the resource to be started (and in the case of promotable clone resources, promoted if appropriate). "Unpromoted" allows the resource to be started, but only in the unpromoted role if the resource is promotable. "Promoted" is equivalent to "Started". State the cluster should attempt to keep this resource in If false, the cluster will not start, stop, promote, or demote the resource on any node. Recurring actions for the resource are unaffected. If true, a true value for the maintenance-mode cluster option, the maintenance node attribute, or the maintenance resource meta-attribute overrides this. Whether the cluster is allowed to actively change the resource's state If true, the cluster will not start, stop, promote, or demote the resource on any node, and will pause any recurring monitors (except those specifying role as "Stopped"). If false, a true value for the maintenance-mode cluster option or maintenance node attribute overrides this. If true, the cluster will not schedule any actions involving the resource Score to add to the current node when a resource is already active. This allows running resources to stay where they are, even if they would be placed elsewhere if they were being started from a stopped state. The default is 1 for individual clone instances, and 0 for all other resources. Score to add to the current node when a resource is already active Conditions under which the resource can be started. "nothing" means the cluster can always start this resource. "quorum" means the cluster can start this resource only if a majority of the configured nodes are active. "fencing" means the cluster can start this resource only if a majority of the configured nodes are active and any failed or unknown nodes have been fenced. "unfencing" means the cluster can start this resource only if a majority of the configured nodes are active and any failed or unknown nodes have been fenced, and only on nodes that have been unfenced. The default is "quorum" for resources with a class of stonith; otherwise, "unfencing" if unfencing is active in the cluster; otherwise, "fencing" if the stonith-enabled cluster option is true; otherwise, "quorum". Conditions under which the resource can be started Number of failures that may occur for this resource on a node, before that node is marked ineligible to host this resource. A value of 0 indicates that this feature is disabled (the node will never be marked ineligible). By contrast, the cluster treats "INFINITY" (the default) as a very large but finite number. This option has an effect only if the failed operation specifies its on-fail attribute as "restart" (the default), and additionally for failed start operations, if the start-failure-is-fatal cluster property is set to false. Number of failures on a node before the resource becomes ineligible to run there. Number of seconds after a failed action for this resource before acting as if the failure had not occurred, and potentially allowing the resource back to the node on which it failed. A value of 0 indicates that this feature is disabled. Number of seconds before acting as if a failure had not occurred What to do if the cluster finds the resource active on more than one node. "block" means to mark the resource as unmanaged. "stop_only" means to stop all active instances of this resource and leave them stopped. "stop_start" means to stop all active instances of this resource and start the resource in one location only. "stop_unexpected" means to stop all active instances of this resource except where the resource should be active. (This should be used only when extra instances are not expected to disrupt existing instances, and the resource agent's monitor of an existing instance is capable of detecting any problems that could be caused. Note that any resources ordered after this one will still need to be restarted.) What to do if the cluster finds the resource active on more than one node Whether the cluster should try to "live migrate" this resource when it needs to be moved. The default is true for ocf:pacemaker:remote resources, and false otherwise. Whether the cluster should try to "live migrate" this resource when it needs to be moved Whether the resource should be allowed to run on a node even if the node's health score would otherwise prevent it Whether the resource should be allowed to run on a node even if the node's health score would otherwise prevent it Whether to check user-defined node attributes on the physical host where a container is running or on the local node. This is usually set for a bundle resource and inherited by the bundle's primitive resource. A value of "host" means to check user-defined node attributes on the underlying physical host. Any other value means to check user-defined node attributes on the local node (for a bundled primitive resource, this is the bundle node). Where to check user-defined node attributes Name of the Pacemaker Remote guest node this resource is associated with, if any. If specified, this both enables the resource as a guest node and defines the unique name used to identify the guest node. The guest must be configured to run the Pacemaker Remote daemon when it is started. WARNING: This value cannot overlap with any resource or node IDs. Name of the Pacemaker Remote guest node this resource is associated with, if any If remote-node is specified, the IP address or hostname used to connect to the guest via Pacemaker Remote. The Pacemaker Remote daemon on the guest must be configured to accept connections on this address. The default is the value of the remote-node meta-attribute. If remote-node is specified, the IP address or hostname used to connect to the guest via Pacemaker Remote If remote-node is specified, the port on the guest used for its Pacemaker Remote connection. The Pacemaker Remote daemon on the guest must be configured to listen on this port. If remote-node is specified, port on the guest used for its Pacemaker Remote connection If remote-node is specified, how long before a pending Pacemaker Remote guest connection times out. If remote-node is specified, how long before a pending Pacemaker Remote guest connection times out. If remote-node is specified, this acts as the allow-migrate meta-attribute for the implicit remote connection resource (ocf:pacemaker:remote). If remote-node is specified, this acts as the allow-migrate meta-attribute for the implicit remote connection resource (ocf:pacemaker:remote). =#=#=#= End test: List all available primitive meta-attributes (XML) - OK (0) =#=#=#= * Passed: crm_resource - List all available primitive meta-attributes (XML) =#=#=#= Begin test: List non-advanced fencing parameters =#=#=#= Fencing resource common parameters Special parameters that are available for all fencing resources, regardless of type. They are processed by Pacemaker, rather than by the fence agent or the fencing library. * pcmk_host_map: A mapping of node names to port numbers for devices that do not support node names. * For example, "node1:1;node2:2,3" would tell the cluster to use port 1 for node1 and ports 2 and 3 for node2. * Possible values: string (no default) * pcmk_host_list: Nodes targeted by this device * Comma-separated list of nodes that can be targeted by this device (for example, "node1,node2,node3"). If pcmk_host_check is "static-list", either this or pcmk_host_map must be set. * Possible values: string (no default) * pcmk_host_check: How to determine which nodes can be targeted by the device * Use "dynamic-list" to query the device via the 'list' command; "static-list" to check the pcmk_host_list attribute; "status" to query the device via the 'status' command; or "none" to assume every device can fence every node. The default value is "static-list" if pcmk_host_map or pcmk_host_list is set; otherwise "dynamic-list" if the device supports the list operation; otherwise "status" if the device supports the status operation; otherwise "none" * Possible values: "dynamic-list", "static-list", "status", "none" * pcmk_delay_max: Enable a delay of no more than the time specified before executing fencing actions. * Enable a delay of no more than the time specified before executing fencing actions. Pacemaker derives the overall delay by taking the value of pcmk_delay_base and adding a random delay value such that the sum is kept below this maximum. * Possible values: duration (default: ) * pcmk_delay_base: Enable a base delay for fencing actions and specify base delay value. * This enables a static delay for fencing actions, which can help avoid "death matches" where two nodes try to fence each other at the same time. If pcmk_delay_max is also used, a random delay will be added such that the total delay is kept below that value. This can be set to a single time value to apply to any node targeted by this device (useful if a separate device is configured for each target), or to a node map (for example, "node1:1s;node2:5") to set a different value for each target. * Possible values: string (default: ) * pcmk_action_limit: The maximum number of actions can be performed in parallel on this device * If the concurrent-fencing cluster property is "true", this specifies the maximum number of actions that can be performed in parallel on this device. A value of -1 means unlimited. * Possible values: integer (default: ) =#=#=#= End test: List non-advanced fencing parameters - OK (0) =#=#=#= * Passed: crm_resource - List non-advanced fencing parameters =#=#=#= Begin test: List non-advanced fencing parameters (XML) =#=#=#= 1.1 Special parameters that are available for all fencing resources, regardless of type. They are processed by Pacemaker, rather than by the fence agent or the fencing library. Fencing resource common parameters If the fencing agent metadata advertises support for the "port" or "plug" parameter, that will be used as the default, otherwise "none" will be used, which tells the cluster not to supply any additional parameters. Name of agent parameter that should be set to the fencing target For example, "node1:1;node2:2,3" would tell the cluster to use port 1 for node1 and ports 2 and 3 for node2. A mapping of node names to port numbers for devices that do not support node names. Comma-separated list of nodes that can be targeted by this device (for example, "node1,node2,node3"). If pcmk_host_check is "static-list", either this or pcmk_host_map must be set. Nodes targeted by this device Use "dynamic-list" to query the device via the 'list' command; "static-list" to check the pcmk_host_list attribute; "status" to query the device via the 'status' command; or "none" to assume every device can fence every node. The default value is "static-list" if pcmk_host_map or pcmk_host_list is set; otherwise "dynamic-list" if the device supports the list operation; otherwise "status" if the device supports the status operation; otherwise "none" How to determine which nodes can be targeted by the device Enable a delay of no more than the time specified before executing fencing actions. Pacemaker derives the overall delay by taking the value of pcmk_delay_base and adding a random delay value such that the sum is kept below this maximum. Enable a delay of no more than the time specified before executing fencing actions. This enables a static delay for fencing actions, which can help avoid "death matches" where two nodes try to fence each other at the same time. If pcmk_delay_max is also used, a random delay will be added such that the total delay is kept below that value. This can be set to a single time value to apply to any node targeted by this device (useful if a separate device is configured for each target), or to a node map (for example, "node1:1s;node2:5") to set a different value for each target. Enable a base delay for fencing actions and specify base delay value. If the concurrent-fencing cluster property is "true", this specifies the maximum number of actions that can be performed in parallel on this device. A value of -1 means unlimited. The maximum number of actions can be performed in parallel on this device Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'reboot' action. An alternate command to run instead of 'reboot' Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'reboot' actions. Specify an alternate timeout to use for 'reboot' actions instead of stonith-timeout Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'reboot' action before giving up. The maximum number of times to try the 'reboot' command within the timeout period Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'off' action. An alternate command to run instead of 'off' Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'off' actions. Specify an alternate timeout to use for 'off' actions instead of stonith-timeout Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'off' action before giving up. The maximum number of times to try the 'off' command within the timeout period Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'on' action. An alternate command to run instead of 'on' Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'on' actions. Specify an alternate timeout to use for 'on' actions instead of stonith-timeout Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'on' action before giving up. The maximum number of times to try the 'on' command within the timeout period Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'list' action. An alternate command to run instead of 'list' Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'list' actions. Specify an alternate timeout to use for 'list' actions instead of stonith-timeout Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'list' action before giving up. The maximum number of times to try the 'list' command within the timeout period Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'monitor' action. An alternate command to run instead of 'monitor' Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'monitor' actions. Specify an alternate timeout to use for 'monitor' actions instead of stonith-timeout Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'monitor' action before giving up. The maximum number of times to try the 'monitor' command within the timeout period Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'status' action. An alternate command to run instead of 'status' Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'status' actions. Specify an alternate timeout to use for 'status' actions instead of stonith-timeout Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'status' action before giving up. The maximum number of times to try the 'status' command within the timeout period =#=#=#= End test: List non-advanced fencing parameters (XML) - OK (0) =#=#=#= * Passed: crm_resource - List non-advanced fencing parameters (XML) =#=#=#= Begin test: List all available fencing parameters =#=#=#= Fencing resource common parameters Special parameters that are available for all fencing resources, regardless of type. They are processed by Pacemaker, rather than by the fence agent or the fencing library. * pcmk_host_map: A mapping of node names to port numbers for devices that do not support node names. * For example, "node1:1;node2:2,3" would tell the cluster to use port 1 for node1 and ports 2 and 3 for node2. * Possible values: string (no default) * pcmk_host_list: Nodes targeted by this device * Comma-separated list of nodes that can be targeted by this device (for example, "node1,node2,node3"). If pcmk_host_check is "static-list", either this or pcmk_host_map must be set. * Possible values: string (no default) * pcmk_host_check: How to determine which nodes can be targeted by the device * Use "dynamic-list" to query the device via the 'list' command; "static-list" to check the pcmk_host_list attribute; "status" to query the device via the 'status' command; or "none" to assume every device can fence every node. The default value is "static-list" if pcmk_host_map or pcmk_host_list is set; otherwise "dynamic-list" if the device supports the list operation; otherwise "status" if the device supports the status operation; otherwise "none" * Possible values: "dynamic-list", "static-list", "status", "none" * pcmk_delay_max: Enable a delay of no more than the time specified before executing fencing actions. * Enable a delay of no more than the time specified before executing fencing actions. Pacemaker derives the overall delay by taking the value of pcmk_delay_base and adding a random delay value such that the sum is kept below this maximum. * Possible values: duration (default: ) * pcmk_delay_base: Enable a base delay for fencing actions and specify base delay value. * This enables a static delay for fencing actions, which can help avoid "death matches" where two nodes try to fence each other at the same time. If pcmk_delay_max is also used, a random delay will be added such that the total delay is kept below that value. This can be set to a single time value to apply to any node targeted by this device (useful if a separate device is configured for each target), or to a node map (for example, "node1:1s;node2:5") to set a different value for each target. * Possible values: string (default: ) * pcmk_action_limit: The maximum number of actions can be performed in parallel on this device * If the concurrent-fencing cluster property is "true", this specifies the maximum number of actions that can be performed in parallel on this device. A value of -1 means unlimited. * Possible values: integer (default: ) * ADVANCED OPTIONS: * pcmk_host_argument: Name of agent parameter that should be set to the fencing target * If the fencing agent metadata advertises support for the "port" or "plug" parameter, that will be used as the default, otherwise "none" will be used, which tells the cluster not to supply any additional parameters. * Possible values: string (no default) * pcmk_reboot_action: An alternate command to run instead of 'reboot' * Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'reboot' action. * Possible values: string (default: ) * pcmk_reboot_timeout: Specify an alternate timeout to use for 'reboot' actions instead of stonith-timeout * Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'reboot' actions. * Possible values: timeout (default: ) * pcmk_reboot_retries: The maximum number of times to try the 'reboot' command within the timeout period * Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'reboot' action before giving up. * Possible values: integer (default: ) * pcmk_off_action: An alternate command to run instead of 'off' * Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'off' action. * Possible values: string (default: ) * pcmk_off_timeout: Specify an alternate timeout to use for 'off' actions instead of stonith-timeout * Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'off' actions. * Possible values: timeout (default: ) * pcmk_off_retries: The maximum number of times to try the 'off' command within the timeout period * Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'off' action before giving up. * Possible values: integer (default: ) * pcmk_on_action: An alternate command to run instead of 'on' * Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'on' action. * Possible values: string (default: ) * pcmk_on_timeout: Specify an alternate timeout to use for 'on' actions instead of stonith-timeout * Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'on' actions. * Possible values: timeout (default: ) * pcmk_on_retries: The maximum number of times to try the 'on' command within the timeout period * Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'on' action before giving up. * Possible values: integer (default: ) * pcmk_list_action: An alternate command to run instead of 'list' * Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'list' action. * Possible values: string (default: ) * pcmk_list_timeout: Specify an alternate timeout to use for 'list' actions instead of stonith-timeout * Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'list' actions. * Possible values: timeout (default: ) * pcmk_list_retries: The maximum number of times to try the 'list' command within the timeout period * Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'list' action before giving up. * Possible values: integer (default: ) * pcmk_monitor_action: An alternate command to run instead of 'monitor' * Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'monitor' action. * Possible values: string (default: ) * pcmk_monitor_timeout: Specify an alternate timeout to use for 'monitor' actions instead of stonith-timeout * Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'monitor' actions. * Possible values: timeout (default: ) * pcmk_monitor_retries: The maximum number of times to try the 'monitor' command within the timeout period * Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'monitor' action before giving up. * Possible values: integer (default: ) * pcmk_status_action: An alternate command to run instead of 'status' * Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'status' action. * Possible values: string (default: ) * pcmk_status_timeout: Specify an alternate timeout to use for 'status' actions instead of stonith-timeout * Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'status' actions. * Possible values: timeout (default: ) * pcmk_status_retries: The maximum number of times to try the 'status' command within the timeout period * Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'status' action before giving up. * Possible values: integer (default: ) =#=#=#= End test: List all available fencing parameters - OK (0) =#=#=#= * Passed: crm_resource - List all available fencing parameters =#=#=#= Begin test: List all available fencing parameters (XML) =#=#=#= 1.1 Special parameters that are available for all fencing resources, regardless of type. They are processed by Pacemaker, rather than by the fence agent or the fencing library. Fencing resource common parameters If the fencing agent metadata advertises support for the "port" or "plug" parameter, that will be used as the default, otherwise "none" will be used, which tells the cluster not to supply any additional parameters. Name of agent parameter that should be set to the fencing target For example, "node1:1;node2:2,3" would tell the cluster to use port 1 for node1 and ports 2 and 3 for node2. A mapping of node names to port numbers for devices that do not support node names. Comma-separated list of nodes that can be targeted by this device (for example, "node1,node2,node3"). If pcmk_host_check is "static-list", either this or pcmk_host_map must be set. Nodes targeted by this device Use "dynamic-list" to query the device via the 'list' command; "static-list" to check the pcmk_host_list attribute; "status" to query the device via the 'status' command; or "none" to assume every device can fence every node. The default value is "static-list" if pcmk_host_map or pcmk_host_list is set; otherwise "dynamic-list" if the device supports the list operation; otherwise "status" if the device supports the status operation; otherwise "none" How to determine which nodes can be targeted by the device Enable a delay of no more than the time specified before executing fencing actions. Pacemaker derives the overall delay by taking the value of pcmk_delay_base and adding a random delay value such that the sum is kept below this maximum. Enable a delay of no more than the time specified before executing fencing actions. This enables a static delay for fencing actions, which can help avoid "death matches" where two nodes try to fence each other at the same time. If pcmk_delay_max is also used, a random delay will be added such that the total delay is kept below that value. This can be set to a single time value to apply to any node targeted by this device (useful if a separate device is configured for each target), or to a node map (for example, "node1:1s;node2:5") to set a different value for each target. Enable a base delay for fencing actions and specify base delay value. If the concurrent-fencing cluster property is "true", this specifies the maximum number of actions that can be performed in parallel on this device. A value of -1 means unlimited. The maximum number of actions can be performed in parallel on this device Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'reboot' action. An alternate command to run instead of 'reboot' Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'reboot' actions. Specify an alternate timeout to use for 'reboot' actions instead of stonith-timeout Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'reboot' action before giving up. The maximum number of times to try the 'reboot' command within the timeout period Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'off' action. An alternate command to run instead of 'off' Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'off' actions. Specify an alternate timeout to use for 'off' actions instead of stonith-timeout Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'off' action before giving up. The maximum number of times to try the 'off' command within the timeout period Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'on' action. An alternate command to run instead of 'on' Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'on' actions. Specify an alternate timeout to use for 'on' actions instead of stonith-timeout Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'on' action before giving up. The maximum number of times to try the 'on' command within the timeout period Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'list' action. An alternate command to run instead of 'list' Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'list' actions. Specify an alternate timeout to use for 'list' actions instead of stonith-timeout Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'list' action before giving up. The maximum number of times to try the 'list' command within the timeout period Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'monitor' action. An alternate command to run instead of 'monitor' Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'monitor' actions. Specify an alternate timeout to use for 'monitor' actions instead of stonith-timeout Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'monitor' action before giving up. The maximum number of times to try the 'monitor' command within the timeout period Some devices do not support the standard commands or may provide additional ones. Use this to specify an alternate, device-specific, command that implements the 'status' action. An alternate command to run instead of 'status' Some devices need much more/less time to complete than normal. Use this to specify an alternate, device-specific, timeout for 'status' actions. Specify an alternate timeout to use for 'status' actions instead of stonith-timeout Some devices do not support multiple connections. Operations may "fail" if the device is busy with another task. In that case, Pacemaker will automatically retry the operation if there is time remaining. Use this option to alter the number of times Pacemaker tries a 'status' action before giving up. The maximum number of times to try the 'status' command within the timeout period =#=#=#= End test: List all available fencing parameters (XML) - OK (0) =#=#=#= * Passed: crm_resource - List all available fencing parameters (XML) =#=#=#= Begin test: Create a resource =#=#=#= =#=#=#= Current cib after: Create a resource =#=#=#= =#=#=#= End test: Create a resource - OK (0) =#=#=#= * Passed: cibadmin - Create a resource =#=#=#= Begin test: crm_resource given both -r and resource config =#=#=#= crm_resource: --resource cannot be used with --class, --agent, and --provider =#=#=#= End test: crm_resource given both -r and resource config - Incorrect usage (64) =#=#=#= * Passed: crm_resource - crm_resource given both -r and resource config =#=#=#= Begin test: crm_resource given resource config with invalid action =#=#=#= crm_resource: --class, --agent, and --provider can only be used with --validate and --force-* =#=#=#= End test: crm_resource given resource config with invalid action - Incorrect usage (64) =#=#=#= * Passed: crm_resource - crm_resource given resource config with invalid action =#=#=#= Begin test: 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 Set 'dummy' option: id=dummy-meta_attributes-is-managed set=dummy-meta_attributes name=is-managed value=false =#=#=#= Current cib after: Create a resource meta attribute =#=#=#= =#=#=#= End test: Create a resource meta attribute - OK (0) =#=#=#= * Passed: crm_resource - Create a resource meta attribute =#=#=#= Begin test: 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 false =#=#=#= Current cib after: Query a resource meta attribute =#=#=#= =#=#=#= End test: Query a resource meta attribute - OK (0) =#=#=#= * Passed: crm_resource - Query a resource meta attribute =#=#=#= Begin test: 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-is-managed name=is-managed =#=#=#= Current cib after: Remove a resource meta attribute =#=#=#= =#=#=#= End test: Remove a resource meta attribute - OK (0) =#=#=#= * Passed: crm_resource - Remove a resource meta attribute =#=#=#= Begin test: Create another resource meta attribute (XML) =#=#=#= 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 =#=#=#= End test: Create another resource meta attribute (XML) - OK (0) =#=#=#= * Passed: crm_resource - Create another resource meta attribute (XML) =#=#=#= Begin test: Show why a resource is not running (XML) =#=#=#= 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 =#=#=#= End test: Show why a resource is not running (XML) - OK (0) =#=#=#= * Passed: crm_resource - Show why a resource is not running (XML) =#=#=#= Begin test: Remove another resource meta attribute (XML) =#=#=#= 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 =#=#=#= End test: Remove another resource meta attribute (XML) - OK (0) =#=#=#= * Passed: crm_resource - Remove another resource meta attribute (XML) =#=#=#= Begin test: Get a non-existent attribute from a resource element (XML) =#=#=#= 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 Attribute 'nonexistent' not found for 'dummy' =#=#=#= End test: Get a non-existent attribute from a resource element (XML) - OK (0) =#=#=#= * Passed: crm_resource - Get a non-existent attribute from a resource element (XML) =#=#=#= Begin test: Get a non-existent attribute from a resource element =#=#=#= 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 Attribute 'nonexistent' not found for 'dummy' =#=#=#= Current cib after: Get a non-existent attribute from a resource element =#=#=#= =#=#=#= End test: Get a non-existent attribute from a resource element - OK (0) =#=#=#= * Passed: crm_resource - Get a non-existent attribute from a resource element =#=#=#= Begin test: Get a non-existent attribute from a resource element (XML) =#=#=#= 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 Attribute 'nonexistent' not found for 'dummy' =#=#=#= Current cib after: Get a non-existent attribute from a resource element (XML) =#=#=#= =#=#=#= End test: Get a non-existent attribute from a resource element (XML) - OK (0) =#=#=#= * Passed: crm_resource - Get a non-existent attribute from a resource element (XML) =#=#=#= Begin test: Get an existent attribute from a resource element =#=#=#= 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 ocf =#=#=#= Current cib after: Get an existent attribute from a resource element =#=#=#= =#=#=#= End test: Get an existent attribute from a resource element - OK (0) =#=#=#= * Passed: crm_resource - Get an existent attribute from a resource element =#=#=#= Begin test: Set a non-existent attribute for a resource element (XML) =#=#=#= 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 =#=#=#= Current cib after: Set a non-existent attribute for a resource element (XML) =#=#=#= =#=#=#= End test: Set a non-existent attribute for a resource element (XML) - OK (0) =#=#=#= * Passed: crm_resource - Set a non-existent attribute for a resource element (XML) =#=#=#= Begin test: Set an existent attribute for a resource element (XML) =#=#=#= 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 =#=#=#= Current cib after: Set an existent attribute for a resource element (XML) =#=#=#= - + =#=#=#= End test: Set an existent attribute for a resource element (XML) - OK (0) =#=#=#= * Passed: crm_resource - Set an existent attribute for a resource element (XML) =#=#=#= Begin test: Delete an existent attribute for a resource element (XML) =#=#=#= 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 =#=#=#= Current cib after: Delete an existent attribute for a resource element (XML) =#=#=#= - + =#=#=#= End test: Delete an existent attribute for a resource element (XML) - OK (0) =#=#=#= * Passed: crm_resource - Delete an existent attribute for a resource element (XML) =#=#=#= Begin test: Delete a non-existent attribute for a resource element (XML) =#=#=#= 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 =#=#=#= Current cib after: Delete a non-existent attribute for a resource element (XML) =#=#=#= - + =#=#=#= End test: Delete a non-existent attribute for a resource element (XML) - OK (0) =#=#=#= * Passed: crm_resource - Delete a non-existent attribute for a resource element (XML) =#=#=#= Begin test: Set a non-existent attribute for a resource element =#=#=#= 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 Set attribute: name=description value=test_description =#=#=#= Current cib after: Set a non-existent attribute for a resource element =#=#=#= - + =#=#=#= End test: Set a non-existent attribute for a resource element - OK (0) =#=#=#= * Passed: crm_resource - Set a non-existent attribute for a resource element =#=#=#= Begin test: Set an existent attribute for a resource element =#=#=#= 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 Set attribute: name=description value=test_description =#=#=#= Current cib after: Set an existent attribute for a resource element =#=#=#= - + =#=#=#= End test: Set an existent attribute for a resource element - OK (0) =#=#=#= * Passed: crm_resource - Set an existent attribute for a resource element =#=#=#= Begin test: Delete an existent attribute for a resource element =#=#=#= 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 attribute: description =#=#=#= Current cib after: Delete an existent attribute for a resource element =#=#=#= - + =#=#=#= End test: Delete an existent attribute for a resource element - OK (0) =#=#=#= * Passed: crm_resource - Delete an existent attribute for a resource element =#=#=#= Begin test: Delete a non-existent attribute for a resource element =#=#=#= 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 attribute: description =#=#=#= Current cib after: Delete a non-existent attribute for a resource element =#=#=#= - + =#=#=#= End test: Delete a non-existent attribute for a resource element - OK (0) =#=#=#= * Passed: crm_resource - Delete a non-existent attribute for a resource element =#=#=#= Begin test: Create a resource 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 Set 'dummy' option: id=dummy-instance_attributes-delay set=dummy-instance_attributes name=delay value=10s =#=#=#= Current cib after: Create a resource attribute =#=#=#= - + =#=#=#= End test: Create a resource attribute - OK (0) =#=#=#= * Passed: crm_resource - Create a resource attribute =#=#=#= Begin test: List the configured resources =#=#=#= 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 Full List of Resources: * dummy (ocf:pacemaker:Dummy): Stopped =#=#=#= Current cib after: List the configured resources =#=#=#= - + =#=#=#= End test: List the configured resources - OK (0) =#=#=#= * Passed: crm_resource - List the configured resources =#=#=#= Begin test: List the configured resources (XML) =#=#=#= 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 =#=#=#= Current cib after: List the configured resources (XML) =#=#=#= - + =#=#=#= End test: List the configured resources (XML) - OK (0) =#=#=#= * Passed: crm_resource - List the configured resources (XML) =#=#=#= Begin test: Implicitly list the configured resources =#=#=#= 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 Full List of Resources: * dummy (ocf:pacemaker:Dummy): Stopped =#=#=#= End test: Implicitly list the configured resources - OK (0) =#=#=#= * Passed: crm_resource - Implicitly list the configured resources =#=#=#= Begin test: List IDs of instantiated resources =#=#=#= 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 dummy =#=#=#= End test: List IDs of instantiated resources - OK (0) =#=#=#= * Passed: crm_resource - List IDs of instantiated resources =#=#=#= Begin test: Show XML configuration of resource =#=#=#= 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 dummy (ocf:pacemaker:Dummy): Stopped Resource XML: =#=#=#= End test: Show XML configuration of resource - OK (0) =#=#=#= * Passed: crm_resource - Show XML configuration of resource =#=#=#= Begin test: Show XML configuration of resource (XML) =#=#=#= 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 ]]> =#=#=#= End test: Show XML configuration of resource (XML) - OK (0) =#=#=#= * Passed: crm_resource - Show XML configuration of resource (XML) =#=#=#= Begin test: Require a destination when migrating a resource that is stopped =#=#=#= 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 crm_resource: Resource 'dummy' not moved: active in 0 locations. To prevent 'dummy' from running on a specific location, specify a node. =#=#=#= Current cib after: Require a destination when migrating a resource that is stopped =#=#=#= - + =#=#=#= End test: Require a destination when migrating a resource that is stopped - Incorrect usage (64) =#=#=#= * Passed: crm_resource - Require a destination when migrating a resource that is stopped =#=#=#= Begin test: Don't support migration to non-existent locations =#=#=#= 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 crm_resource: Node 'i.do.not.exist' not found Error performing operation: No such object =#=#=#= Current cib after: Don't support migration to non-existent locations =#=#=#= - + =#=#=#= End test: Don't support migration to non-existent locations - No such object (105) =#=#=#= * Passed: crm_resource - Don't support migration to non-existent locations =#=#=#= Begin test: Create a fencing resource =#=#=#= =#=#=#= Current cib after: Create a fencing resource =#=#=#= - + =#=#=#= End test: Create a fencing resource - OK (0) =#=#=#= * Passed: cibadmin - Create a fencing resource =#=#=#= Begin test: Bring resources online =#=#=#= Current cluster status: * Node List: * Online: [ node1 ] * Full List of Resources: * dummy (ocf:pacemaker:Dummy): Stopped * Fence (stonith:fence_true): Stopped Transition Summary: * Start dummy ( node1 ) * Start Fence ( node1 ) Executing Cluster Transition: * Resource action: dummy monitor on node1 * Resource action: Fence monitor on node1 * Resource action: dummy start on node1 * Resource action: Fence start on node1 Revised Cluster Status: * Node List: * Online: [ node1 ] * Full List of Resources: * dummy (ocf:pacemaker:Dummy): Started node1 * Fence (stonith:fence_true): Started node1 =#=#=#= Current cib after: Bring resources online =#=#=#= - + =#=#=#= End test: Bring resources online - OK (0) =#=#=#= * Passed: crm_simulate - Bring resources online =#=#=#= Begin test: Try to move a resource to its existing location =#=#=#= crm_resource: Error performing operation: Requested item already exists =#=#=#= Current cib after: Try to move a resource to its existing location =#=#=#= - + =#=#=#= End test: Try to move a resource to its existing location - Requested item already exists (108) =#=#=#= * Passed: crm_resource - Try to move a resource to its existing location =#=#=#= Begin test: Try to move a resource that doesn't exist =#=#=#= crm_resource: Resource 'xyz' not found Error performing operation: No such object =#=#=#= End test: Try to move a resource that doesn't exist - No such object (105) =#=#=#= * Passed: crm_resource - Try to move a resource that doesn't exist =#=#=#= Begin test: Move a resource from its existing location =#=#=#= WARNING: Creating rsc_location constraint 'cli-ban-dummy-on-node1' with a score of -INFINITY for resource dummy on node1. This will prevent dummy from running on node1 until the constraint is removed using the clear option or by editing the CIB with an appropriate tool. This will be the case even if node1 is the last node in the cluster =#=#=#= Current cib after: Move a resource from its existing location =#=#=#= - + =#=#=#= End test: Move a resource from its existing location - OK (0) =#=#=#= * Passed: crm_resource - Move a resource from its existing location =#=#=#= Begin test: Clear out constraints generated by --move =#=#=#= Removing constraint: cli-ban-dummy-on-node1 =#=#=#= Current cib after: Clear out constraints generated by --move =#=#=#= - + =#=#=#= End test: Clear out constraints generated by --move - OK (0) =#=#=#= * Passed: crm_resource - Clear out constraints generated by --move =#=#=#= Begin test: Ban a resource on unknown node =#=#=#= crm_resource: Node 'host1' not found Error performing operation: No such object =#=#=#= End test: Ban a resource on unknown node - No such object (105) =#=#=#= * Passed: crm_resource - Ban a resource on unknown node =#=#=#= Begin test: Create two more nodes and bring them online =#=#=#= Current cluster status: * Node List: * Online: [ node1 ] * Full List of Resources: * dummy (ocf:pacemaker:Dummy): Started node1 * Fence (stonith:fence_true): Started node1 Performing Requested Modifications: * Bringing node node2 online * Bringing node node3 online Transition Summary: * Move Fence ( node1 -> node2 ) Executing Cluster Transition: * Resource action: dummy monitor on node3 * Resource action: dummy monitor on node2 * Resource action: Fence stop on node1 * Resource action: Fence monitor on node3 * Resource action: Fence monitor on node2 * Resource action: Fence start on node2 Revised Cluster Status: * Node List: * Online: [ node1 node2 node3 ] * Full List of Resources: * dummy (ocf:pacemaker:Dummy): Started node1 * Fence (stonith:fence_true): Started node2 =#=#=#= Current cib after: Create two more nodes and bring them online =#=#=#= - + =#=#=#= End test: Create two more nodes and bring them online - OK (0) =#=#=#= * Passed: crm_simulate - Create two more nodes and bring them online =#=#=#= Begin test: Ban dummy from node1 =#=#=#= WARNING: Creating rsc_location constraint 'cli-ban-dummy-on-node1' with a score of -INFINITY for resource dummy on node1. This will prevent dummy from running on node1 until the constraint is removed using the clear option or by editing the CIB with an appropriate tool. This will be the case even if node1 is the last node in the cluster =#=#=#= Current cib after: Ban dummy from node1 =#=#=#= - + =#=#=#= End test: Ban dummy from node1 - OK (0) =#=#=#= * Passed: crm_resource - Ban dummy from node1 =#=#=#= Begin test: Show where a resource is running =#=#=#= resource dummy is running on: node1 =#=#=#= End test: Show where a resource is running - OK (0) =#=#=#= * Passed: crm_resource - Show where a resource is running =#=#=#= Begin test: Show constraints on a resource =#=#=#= Locations: * Node node1 (score=-INFINITY, id=cli-ban-dummy-on-node1, rsc=dummy) =#=#=#= End test: Show constraints on a resource - OK (0) =#=#=#= * Passed: crm_resource - Show constraints on a resource =#=#=#= Begin test: Ban dummy from node2 (XML) =#=#=#= =#=#=#= Current cib after: Ban dummy from node2 (XML) =#=#=#= - + =#=#=#= End test: Ban dummy from node2 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Ban dummy from node2 (XML) =#=#=#= Begin test: Relocate resources due to ban =#=#=#= Current cluster status: * Node List: * Online: [ node1 node2 node3 ] * Full List of Resources: * dummy (ocf:pacemaker:Dummy): Started node1 * Fence (stonith:fence_true): Started node2 Transition Summary: * Move dummy ( node1 -> node3 ) Executing Cluster Transition: * Resource action: dummy stop on node1 * Resource action: dummy start on node3 Revised Cluster Status: * Node List: * Online: [ node1 node2 node3 ] * Full List of Resources: * dummy (ocf:pacemaker:Dummy): Started node3 * Fence (stonith:fence_true): Started node2 =#=#=#= Current cib after: Relocate resources due to ban =#=#=#= - + =#=#=#= End test: Relocate resources due to ban - OK (0) =#=#=#= * Passed: crm_simulate - Relocate resources due to ban =#=#=#= Begin test: Move dummy to node1 (XML) =#=#=#= =#=#=#= Current cib after: Move dummy to node1 (XML) =#=#=#= - + =#=#=#= End test: Move dummy to node1 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Move dummy to node1 (XML) =#=#=#= Begin test: Clear implicit constraints for dummy on node2 =#=#=#= Removing constraint: cli-ban-dummy-on-node2 =#=#=#= Current cib after: Clear implicit constraints for dummy on node2 =#=#=#= - + =#=#=#= End test: Clear implicit constraints for dummy on node2 - OK (0) =#=#=#= * Passed: crm_resource - Clear implicit constraints for dummy on node2 =#=#=#= Begin test: Drop the status section =#=#=#= =#=#=#= End test: Drop the status section - OK (0) =#=#=#= * Passed: cibadmin - Drop the status section =#=#=#= Begin test: Create a clone =#=#=#= =#=#=#= End test: Create a clone - OK (0) =#=#=#= * Passed: cibadmin - Create a clone =#=#=#= Begin test: Create a resource meta attribute =#=#=#= Performing update of 'is-managed' on 'test-clone', the parent of 'test-primitive' Set 'test-clone' option: id=test-clone-meta_attributes-is-managed set=test-clone-meta_attributes name=is-managed value=false =#=#=#= Current cib after: Create a resource meta attribute =#=#=#= - + =#=#=#= End test: Create a resource meta attribute - OK (0) =#=#=#= * Passed: crm_resource - Create a resource meta attribute =#=#=#= Begin test: Create a resource meta attribute in the primitive =#=#=#= Set 'test-primitive' option: id=test-primitive-meta_attributes-is-managed set=test-primitive-meta_attributes name=is-managed value=false =#=#=#= Current cib after: Create a resource meta attribute in the primitive =#=#=#= - + =#=#=#= End test: Create a resource meta attribute in the primitive - OK (0) =#=#=#= * Passed: crm_resource - Create a resource meta attribute in the primitive =#=#=#= Begin test: Update resource meta attribute with duplicates =#=#=#= Multiple attributes match name=is-managed Value: false (id=test-primitive-meta_attributes-is-managed) Value: false (id=test-clone-meta_attributes-is-managed) A value for 'is-managed' already exists in child 'test-primitive', performing update on that instead of 'test-clone' Set 'test-primitive' option: id=test-primitive-meta_attributes-is-managed name=is-managed value=true =#=#=#= Current cib after: Update resource meta attribute with duplicates =#=#=#= - + =#=#=#= End test: Update resource meta attribute with duplicates - OK (0) =#=#=#= * Passed: crm_resource - Update resource meta attribute with duplicates =#=#=#= Begin test: Update resource meta attribute with duplicates (force clone) =#=#=#= Set 'test-clone' option: id=test-clone-meta_attributes-is-managed name=is-managed value=true =#=#=#= Current cib after: Update resource meta attribute with duplicates (force clone) =#=#=#= - + =#=#=#= End test: Update resource meta attribute with duplicates (force clone) - OK (0) =#=#=#= * Passed: crm_resource - Update resource meta attribute with duplicates (force clone) =#=#=#= Begin test: Update child resource meta attribute with duplicates =#=#=#= Multiple attributes match name=is-managed Value: true (id=test-primitive-meta_attributes-is-managed) Value: true (id=test-clone-meta_attributes-is-managed) Set 'test-primitive' option: id=test-primitive-meta_attributes-is-managed name=is-managed value=false =#=#=#= Current cib after: Update child resource meta attribute with duplicates =#=#=#= - + =#=#=#= End test: Update child resource meta attribute with duplicates - OK (0) =#=#=#= * Passed: crm_resource - Update child resource meta attribute with duplicates =#=#=#= Begin test: Delete resource meta attribute with duplicates =#=#=#= Multiple attributes match name=is-managed Value: false (id=test-primitive-meta_attributes-is-managed) Value: true (id=test-clone-meta_attributes-is-managed) A value for 'is-managed' already exists in child 'test-primitive', performing delete on that instead of 'test-clone' Deleted 'test-primitive' option: id=test-primitive-meta_attributes-is-managed name=is-managed =#=#=#= Current cib after: Delete resource meta attribute with duplicates =#=#=#= - + =#=#=#= End test: Delete resource meta attribute with duplicates - OK (0) =#=#=#= * Passed: crm_resource - Delete resource meta attribute with duplicates =#=#=#= Begin test: Delete resource meta attribute in parent =#=#=#= Performing delete of 'is-managed' on 'test-clone', the parent of 'test-primitive' Deleted 'test-clone' option: id=test-clone-meta_attributes-is-managed name=is-managed =#=#=#= Current cib after: Delete resource meta attribute in parent =#=#=#= - + =#=#=#= End test: Delete resource meta attribute in parent - OK (0) =#=#=#= * Passed: crm_resource - Delete resource meta attribute in parent =#=#=#= Begin test: Create a resource meta attribute in the primitive =#=#=#= Set 'test-primitive' option: id=test-primitive-meta_attributes-is-managed set=test-primitive-meta_attributes name=is-managed value=false =#=#=#= Current cib after: Create a resource meta attribute in the primitive =#=#=#= - + =#=#=#= End test: Create a resource meta attribute in the primitive - OK (0) =#=#=#= * Passed: crm_resource - Create a resource meta attribute in the primitive =#=#=#= Begin test: Update existing resource meta attribute =#=#=#= A value for 'is-managed' already exists in child 'test-primitive', performing update on that instead of 'test-clone' Set 'test-primitive' option: id=test-primitive-meta_attributes-is-managed name=is-managed value=true =#=#=#= Current cib after: Update existing resource meta attribute =#=#=#= - + =#=#=#= End test: Update existing resource meta attribute - OK (0) =#=#=#= * Passed: crm_resource - Update existing resource meta attribute =#=#=#= Begin test: Create a resource meta attribute in the parent =#=#=#= Set 'test-clone' option: id=test-clone-meta_attributes-is-managed set=test-clone-meta_attributes name=is-managed value=true =#=#=#= Current cib after: Create a resource meta attribute in the parent =#=#=#= - + =#=#=#= End test: Create a resource meta attribute in the parent - OK (0) =#=#=#= * Passed: crm_resource - Create a resource meta attribute in the parent =#=#=#= Begin test: Delete resource parent meta attribute (force) =#=#=#= Deleted 'test-clone' option: id=test-clone-meta_attributes-is-managed name=is-managed =#=#=#= Current cib after: Delete resource parent meta attribute (force) =#=#=#= - + =#=#=#= End test: Delete resource parent meta attribute (force) - OK (0) =#=#=#= * Passed: crm_resource - Delete resource parent meta attribute (force) =#=#=#= Begin test: Delete resource child meta attribute =#=#=#= Multiple attributes match name=is-managed Value: true (id=test-primitive-meta_attributes-is-managed) Value: true (id=test-clone-meta_attributes-is-managed) Deleted 'test-primitive' option: id=test-primitive-meta_attributes-is-managed name=is-managed =#=#=#= Current cib after: Delete resource child meta attribute =#=#=#= - + =#=#=#= End test: Delete resource child meta attribute - OK (0) =#=#=#= * Passed: crm_resource - Delete resource child meta attribute =#=#=#= Begin test: Create the dummy-group resource group =#=#=#= =#=#=#= Current cib after: Create the dummy-group resource group =#=#=#= - + =#=#=#= End test: Create the dummy-group resource group - OK (0) =#=#=#= * Passed: cibadmin - Create the dummy-group resource group =#=#=#= Begin test: Create a resource meta attribute in dummy1 =#=#=#= Set 'dummy1' option: id=dummy1-meta_attributes-is-managed set=dummy1-meta_attributes name=is-managed value=true =#=#=#= Current cib after: Create a resource meta attribute in dummy1 =#=#=#= - + =#=#=#= End test: Create a resource meta attribute in dummy1 - OK (0) =#=#=#= * Passed: crm_resource - Create a resource meta attribute in dummy1 =#=#=#= Begin test: Create a resource meta attribute in dummy-group =#=#=#= Set 'dummy1' option: id=dummy1-meta_attributes-is-managed name=is-managed value=false Set 'dummy-group' option: id=dummy-group-meta_attributes-is-managed set=dummy-group-meta_attributes name=is-managed value=false =#=#=#= Current cib after: Create a resource meta attribute in dummy-group =#=#=#= - + =#=#=#= End test: Create a resource meta attribute in dummy-group - OK (0) =#=#=#= * Passed: crm_resource - Create a resource meta attribute in dummy-group =#=#=#= Begin test: Delete the dummy-group resource group =#=#=#= =#=#=#= Current cib after: Delete the dummy-group resource group =#=#=#= - + =#=#=#= End test: Delete the dummy-group resource group - OK (0) =#=#=#= * Passed: cibadmin - Delete the dummy-group resource group =#=#=#= Begin test: Specify a lifetime when moving a resource =#=#=#= Migration will take effect until: =#=#=#= Current cib after: Specify a lifetime when moving a resource =#=#=#= - + =#=#=#= End test: Specify a lifetime when moving a resource - OK (0) =#=#=#= * Passed: crm_resource - Specify a lifetime when moving a resource =#=#=#= Begin test: Try to move a resource previously moved with a lifetime =#=#=#= =#=#=#= Current cib after: Try to move a resource previously moved with a lifetime =#=#=#= - + =#=#=#= End test: Try to move a resource previously moved with a lifetime - OK (0) =#=#=#= * Passed: crm_resource - Try to move a resource previously moved with a lifetime =#=#=#= Begin test: Ban dummy from node1 for a short time =#=#=#= Migration will take effect until: WARNING: Creating rsc_location constraint 'cli-ban-dummy-on-node1' with a score of -INFINITY for resource dummy on node1. This will prevent dummy from running on node1 until the constraint is removed using the clear option or by editing the CIB with an appropriate tool. This will be the case even if node1 is the last node in the cluster =#=#=#= Current cib after: Ban dummy from node1 for a short time =#=#=#= - + =#=#=#= End test: Ban dummy from node1 for a short time - OK (0) =#=#=#= * Passed: crm_resource - Ban dummy from node1 for a short time =#=#=#= Begin test: Remove expired constraints =#=#=#= Removing constraint: cli-ban-dummy-on-node1 =#=#=#= Current cib after: Remove expired constraints =#=#=#= - + =#=#=#= End test: Remove expired constraints - OK (0) =#=#=#= * Passed: sleep - Remove expired constraints =#=#=#= Begin test: Clear all implicit constraints for dummy =#=#=#= Removing constraint: cli-prefer-dummy =#=#=#= Current cib after: Clear all implicit constraints for dummy =#=#=#= - + =#=#=#= End test: Clear all implicit constraints for dummy - OK (0) =#=#=#= * Passed: crm_resource - Clear all implicit constraints for dummy =#=#=#= Begin test: Set a node health strategy =#=#=#= =#=#=#= Current cib after: Set a node health strategy =#=#=#= - + =#=#=#= End test: Set a node health strategy - OK (0) =#=#=#= * Passed: crm_attribute - Set a node health strategy =#=#=#= Begin test: Set a node health attribute =#=#=#= =#=#=#= Current cib after: Set a node health attribute =#=#=#= - + =#=#=#= End test: Set a node health attribute - OK (0) =#=#=#= * Passed: crm_attribute - Set a node health attribute =#=#=#= Begin test: Show why a resource is not running on an unhealthy node (XML) =#=#=#= =#=#=#= End test: Show why a resource is not running on an unhealthy node (XML) - OK (0) =#=#=#= * Passed: crm_resource - Show why a resource is not running on an unhealthy node (XML) =#=#=#= Begin test: Delete a resource =#=#=#= =#=#=#= Current cib after: Delete a resource =#=#=#= - + =#=#=#= End test: Delete a resource - OK (0) =#=#=#= * Passed: crm_resource - Delete a resource =#=#=#= Begin test: Check locations and constraints for prim1 =#=#=#= =#=#=#= End test: Check locations and constraints for prim1 - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim1 =#=#=#= Begin test: Check locations and constraints for prim1 (XML) =#=#=#= =#=#=#= End test: Check locations and constraints for prim1 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim1 (XML) =#=#=#= Begin test: Recursively check locations and constraints for prim1 =#=#=#= =#=#=#= End test: Recursively check locations and constraints for prim1 - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim1 =#=#=#= Begin test: Recursively check locations and constraints for prim1 (XML) =#=#=#= =#=#=#= End test: Recursively check locations and constraints for prim1 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim1 (XML) =#=#=#= Begin test: Check locations and constraints for prim2 =#=#=#= Locations: * Node cluster01 (score=INFINITY, id=prim2-on-cluster1, rsc=prim2) Resources prim2 is colocated with: * prim3 (score=INFINITY, id=colocation-prim2-prim3-INFINITY) =#=#=#= End test: Check locations and constraints for prim2 - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim2 =#=#=#= Begin test: Check locations and constraints for prim2 (XML) =#=#=#= =#=#=#= End test: Check locations and constraints for prim2 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim2 (XML) =#=#=#= Begin test: Recursively check locations and constraints for prim2 =#=#=#= Locations: * Node cluster01 (score=INFINITY, id=prim2-on-cluster1, rsc=prim2) Resources prim2 is colocated with: * prim3 (score=INFINITY, id=colocation-prim2-prim3-INFINITY) * Resources prim3 is colocated with: * prim4 (score=INFINITY, id=colocation-prim3-prim4-INFINITY) * Locations: * Node cluster02 (score=INFINITY, id=prim4-on-cluster2, rsc=prim4) * Resources prim4 is colocated with: * prim5 (score=INFINITY, id=colocation-prim4-prim5-INFINITY) =#=#=#= End test: Recursively check locations and constraints for prim2 - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim2 =#=#=#= Begin test: Recursively check locations and constraints for prim2 (XML) =#=#=#= =#=#=#= End test: Recursively check locations and constraints for prim2 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim2 (XML) =#=#=#= Begin test: Check locations and constraints for prim3 =#=#=#= Resources colocated with prim3: * prim2 (score=INFINITY, id=colocation-prim2-prim3-INFINITY) * Locations: * Node cluster01 (score=INFINITY, id=prim2-on-cluster1, rsc=prim2) Resources prim3 is colocated with: * prim4 (score=INFINITY, id=colocation-prim3-prim4-INFINITY) * Locations: * Node cluster02 (score=INFINITY, id=prim4-on-cluster2, rsc=prim4) =#=#=#= End test: Check locations and constraints for prim3 - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim3 =#=#=#= Begin test: Check locations and constraints for prim3 (XML) =#=#=#= =#=#=#= End test: Check locations and constraints for prim3 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim3 (XML) =#=#=#= Begin test: Recursively check locations and constraints for prim3 =#=#=#= Resources colocated with prim3: * prim2 (score=INFINITY, id=colocation-prim2-prim3-INFINITY) * Locations: * Node cluster01 (score=INFINITY, id=prim2-on-cluster1, rsc=prim2) Resources prim3 is colocated with: * prim4 (score=INFINITY, id=colocation-prim3-prim4-INFINITY) * Locations: * Node cluster02 (score=INFINITY, id=prim4-on-cluster2, rsc=prim4) * Resources prim4 is colocated with: * prim5 (score=INFINITY, id=colocation-prim4-prim5-INFINITY) =#=#=#= End test: Recursively check locations and constraints for prim3 - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim3 =#=#=#= Begin test: Recursively check locations and constraints for prim3 (XML) =#=#=#= =#=#=#= End test: Recursively check locations and constraints for prim3 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim3 (XML) =#=#=#= Begin test: Check locations and constraints for prim4 =#=#=#= Locations: * Node cluster02 (score=INFINITY, id=prim4-on-cluster2, rsc=prim4) Resources colocated with prim4: * prim10 (score=INFINITY, id=colocation-prim10-prim4-INFINITY) * prim3 (score=INFINITY, id=colocation-prim3-prim4-INFINITY) Resources prim4 is colocated with: * prim5 (score=INFINITY, id=colocation-prim4-prim5-INFINITY) =#=#=#= End test: Check locations and constraints for prim4 - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim4 =#=#=#= Begin test: Check locations and constraints for prim4 (XML) =#=#=#= =#=#=#= End test: Check locations and constraints for prim4 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim4 (XML) =#=#=#= Begin test: Recursively check locations and constraints for prim4 =#=#=#= Locations: * Node cluster02 (score=INFINITY, id=prim4-on-cluster2, rsc=prim4) Resources colocated with prim4: * prim10 (score=INFINITY, id=colocation-prim10-prim4-INFINITY) * prim3 (score=INFINITY, id=colocation-prim3-prim4-INFINITY) * Resources colocated with prim3: * prim2 (score=INFINITY, id=colocation-prim2-prim3-INFINITY) * Locations: * Node cluster01 (score=INFINITY, id=prim2-on-cluster1, rsc=prim2) Resources prim4 is colocated with: * prim5 (score=INFINITY, id=colocation-prim4-prim5-INFINITY) =#=#=#= End test: Recursively check locations and constraints for prim4 - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim4 =#=#=#= Begin test: Recursively check locations and constraints for prim4 (XML) =#=#=#= =#=#=#= End test: Recursively check locations and constraints for prim4 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim4 (XML) =#=#=#= Begin test: Check locations and constraints for prim5 =#=#=#= Resources colocated with prim5: * prim4 (score=INFINITY, id=colocation-prim4-prim5-INFINITY) * Locations: * Node cluster02 (score=INFINITY, id=prim4-on-cluster2, rsc=prim4) =#=#=#= End test: Check locations and constraints for prim5 - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim5 =#=#=#= Begin test: Check locations and constraints for prim5 (XML) =#=#=#= =#=#=#= End test: Check locations and constraints for prim5 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim5 (XML) =#=#=#= Begin test: Recursively check locations and constraints for prim5 =#=#=#= Resources colocated with prim5: * prim4 (score=INFINITY, id=colocation-prim4-prim5-INFINITY) * Locations: * Node cluster02 (score=INFINITY, id=prim4-on-cluster2, rsc=prim4) * Resources colocated with prim4: * prim10 (score=INFINITY, id=colocation-prim10-prim4-INFINITY) * prim3 (score=INFINITY, id=colocation-prim3-prim4-INFINITY) * Resources colocated with prim3: * prim2 (score=INFINITY, id=colocation-prim2-prim3-INFINITY) * Locations: * Node cluster01 (score=INFINITY, id=prim2-on-cluster1, rsc=prim2) =#=#=#= End test: Recursively check locations and constraints for prim5 - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim5 =#=#=#= Begin test: Recursively check locations and constraints for prim5 (XML) =#=#=#= =#=#=#= End test: Recursively check locations and constraints for prim5 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim5 (XML) =#=#=#= Begin test: Check locations and constraints for prim6 =#=#=#= Locations: * Node cluster02 (score=-INFINITY, id=prim6-not-on-cluster2, rsc=prim6) =#=#=#= End test: Check locations and constraints for prim6 - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim6 =#=#=#= Begin test: Check locations and constraints for prim6 (XML) =#=#=#= =#=#=#= End test: Check locations and constraints for prim6 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim6 (XML) =#=#=#= Begin test: Recursively check locations and constraints for prim6 =#=#=#= Locations: * Node cluster02 (score=-INFINITY, id=prim6-not-on-cluster2, rsc=prim6) =#=#=#= End test: Recursively check locations and constraints for prim6 - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim6 =#=#=#= Begin test: Recursively check locations and constraints for prim6 (XML) =#=#=#= =#=#=#= End test: Recursively check locations and constraints for prim6 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim6 (XML) =#=#=#= Begin test: Check locations and constraints for prim7 =#=#=#= Resources prim7 is colocated with: * group (score=INFINITY, id=colocation-prim7-group-INFINITY) =#=#=#= End test: Check locations and constraints for prim7 - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim7 =#=#=#= Begin test: Check locations and constraints for prim7 (XML) =#=#=#= =#=#=#= End test: Check locations and constraints for prim7 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim7 (XML) =#=#=#= Begin test: Recursively check locations and constraints for prim7 =#=#=#= Resources prim7 is colocated with: * group (score=INFINITY, id=colocation-prim7-group-INFINITY) =#=#=#= End test: Recursively check locations and constraints for prim7 - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim7 =#=#=#= Begin test: Recursively check locations and constraints for prim7 (XML) =#=#=#= =#=#=#= End test: Recursively check locations and constraints for prim7 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim7 (XML) =#=#=#= Begin test: Check locations and constraints for prim8 =#=#=#= Resources prim8 is colocated with: * gr2 (score=INFINITY, id=colocation-prim8-gr2-INFINITY) =#=#=#= End test: Check locations and constraints for prim8 - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim8 =#=#=#= Begin test: Check locations and constraints for prim8 (XML) =#=#=#= =#=#=#= End test: Check locations and constraints for prim8 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim8 (XML) =#=#=#= Begin test: Recursively check locations and constraints for prim8 =#=#=#= Resources prim8 is colocated with: * gr2 (score=INFINITY, id=colocation-prim8-gr2-INFINITY) =#=#=#= End test: Recursively check locations and constraints for prim8 - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim8 =#=#=#= Begin test: Recursively check locations and constraints for prim8 (XML) =#=#=#= =#=#=#= End test: Recursively check locations and constraints for prim8 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim8 (XML) =#=#=#= Begin test: Check locations and constraints for prim9 =#=#=#= Resources prim9 is colocated with: * clone (score=INFINITY, id=colocation-prim9-clone-INFINITY) =#=#=#= End test: Check locations and constraints for prim9 - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim9 =#=#=#= Begin test: Check locations and constraints for prim9 (XML) =#=#=#= =#=#=#= End test: Check locations and constraints for prim9 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim9 (XML) =#=#=#= Begin test: Recursively check locations and constraints for prim9 =#=#=#= Resources prim9 is colocated with: * clone (score=INFINITY, id=colocation-prim9-clone-INFINITY) =#=#=#= End test: Recursively check locations and constraints for prim9 - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim9 =#=#=#= Begin test: Recursively check locations and constraints for prim9 (XML) =#=#=#= =#=#=#= End test: Recursively check locations and constraints for prim9 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim9 (XML) =#=#=#= Begin test: Check locations and constraints for prim10 =#=#=#= Resources prim10 is colocated with: * prim4 (score=INFINITY, id=colocation-prim10-prim4-INFINITY) * Locations: * Node cluster02 (score=INFINITY, id=prim4-on-cluster2, rsc=prim4) =#=#=#= End test: Check locations and constraints for prim10 - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim10 =#=#=#= Begin test: Check locations and constraints for prim10 (XML) =#=#=#= =#=#=#= End test: Check locations and constraints for prim10 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim10 (XML) =#=#=#= Begin test: Recursively check locations and constraints for prim10 =#=#=#= Resources prim10 is colocated with: * prim4 (score=INFINITY, id=colocation-prim10-prim4-INFINITY) * Locations: * Node cluster02 (score=INFINITY, id=prim4-on-cluster2, rsc=prim4) * Resources prim4 is colocated with: * prim5 (score=INFINITY, id=colocation-prim4-prim5-INFINITY) =#=#=#= End test: Recursively check locations and constraints for prim10 - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim10 =#=#=#= Begin test: Recursively check locations and constraints for prim10 (XML) =#=#=#= =#=#=#= End test: Recursively check locations and constraints for prim10 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim10 (XML) =#=#=#= Begin test: Check locations and constraints for prim11 =#=#=#= Resources colocated with prim11: * prim13 (score=INFINITY, id=colocation-prim13-prim11-INFINITY) Resources prim11 is colocated with: * prim12 (score=INFINITY, id=colocation-prim11-prim12-INFINITY) =#=#=#= End test: Check locations and constraints for prim11 - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim11 =#=#=#= Begin test: Check locations and constraints for prim11 (XML) =#=#=#= =#=#=#= End test: Check locations and constraints for prim11 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim11 (XML) =#=#=#= Begin test: Recursively check locations and constraints for prim11 =#=#=#= Resources colocated with prim11: * prim13 (score=INFINITY, id=colocation-prim13-prim11-INFINITY) * Resources colocated with prim13: * prim12 (score=INFINITY, id=colocation-prim12-prim13-INFINITY) * Resources colocated with prim12: * prim11 (id=colocation-prim11-prim12-INFINITY - loop) Resources prim11 is colocated with: * prim12 (score=INFINITY, id=colocation-prim11-prim12-INFINITY) * Resources prim12 is colocated with: * prim13 (score=INFINITY, id=colocation-prim12-prim13-INFINITY) * Resources prim13 is colocated with: * prim11 (id=colocation-prim13-prim11-INFINITY - loop) =#=#=#= End test: Recursively check locations and constraints for prim11 - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim11 =#=#=#= Begin test: Recursively check locations and constraints for prim11 (XML) =#=#=#= =#=#=#= End test: Recursively check locations and constraints for prim11 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim11 (XML) =#=#=#= Begin test: Check locations and constraints for prim12 =#=#=#= Resources colocated with prim12: * prim11 (score=INFINITY, id=colocation-prim11-prim12-INFINITY) Resources prim12 is colocated with: * prim13 (score=INFINITY, id=colocation-prim12-prim13-INFINITY) =#=#=#= End test: Check locations and constraints for prim12 - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim12 =#=#=#= Begin test: Check locations and constraints for prim12 (XML) =#=#=#= =#=#=#= End test: Check locations and constraints for prim12 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim12 (XML) =#=#=#= Begin test: Recursively check locations and constraints for prim12 =#=#=#= Resources colocated with prim12: * prim11 (score=INFINITY, id=colocation-prim11-prim12-INFINITY) * Resources colocated with prim11: * prim13 (score=INFINITY, id=colocation-prim13-prim11-INFINITY) * Resources colocated with prim13: * prim12 (id=colocation-prim12-prim13-INFINITY - loop) Resources prim12 is colocated with: * prim13 (score=INFINITY, id=colocation-prim12-prim13-INFINITY) * Resources prim13 is colocated with: * prim11 (score=INFINITY, id=colocation-prim13-prim11-INFINITY) * Resources prim11 is colocated with: * prim12 (id=colocation-prim11-prim12-INFINITY - loop) =#=#=#= End test: Recursively check locations and constraints for prim12 - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim12 =#=#=#= Begin test: Recursively check locations and constraints for prim12 (XML) =#=#=#= =#=#=#= End test: Recursively check locations and constraints for prim12 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim12 (XML) =#=#=#= Begin test: Check locations and constraints for prim13 =#=#=#= Resources colocated with prim13: * prim12 (score=INFINITY, id=colocation-prim12-prim13-INFINITY) Resources prim13 is colocated with: * prim11 (score=INFINITY, id=colocation-prim13-prim11-INFINITY) =#=#=#= End test: Check locations and constraints for prim13 - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim13 =#=#=#= Begin test: Check locations and constraints for prim13 (XML) =#=#=#= =#=#=#= End test: Check locations and constraints for prim13 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for prim13 (XML) =#=#=#= Begin test: Recursively check locations and constraints for prim13 =#=#=#= Resources colocated with prim13: * prim12 (score=INFINITY, id=colocation-prim12-prim13-INFINITY) * Resources colocated with prim12: * prim11 (score=INFINITY, id=colocation-prim11-prim12-INFINITY) * Resources colocated with prim11: * prim13 (id=colocation-prim13-prim11-INFINITY - loop) Resources prim13 is colocated with: * prim11 (score=INFINITY, id=colocation-prim13-prim11-INFINITY) * Resources prim11 is colocated with: * prim12 (score=INFINITY, id=colocation-prim11-prim12-INFINITY) * Resources prim12 is colocated with: * prim13 (id=colocation-prim12-prim13-INFINITY - loop) =#=#=#= End test: Recursively check locations and constraints for prim13 - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim13 =#=#=#= Begin test: Recursively check locations and constraints for prim13 (XML) =#=#=#= =#=#=#= End test: Recursively check locations and constraints for prim13 (XML) - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for prim13 (XML) =#=#=#= Begin test: Check locations and constraints for group =#=#=#= Resources colocated with group: * prim7 (score=INFINITY, id=colocation-prim7-group-INFINITY) =#=#=#= End test: Check locations and constraints for group - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for group =#=#=#= Begin test: Check locations and constraints for group (XML) =#=#=#= =#=#=#= End test: Check locations and constraints for group (XML) - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for group (XML) =#=#=#= Begin test: Recursively check locations and constraints for group =#=#=#= Resources colocated with group: * prim7 (score=INFINITY, id=colocation-prim7-group-INFINITY) =#=#=#= End test: Recursively check locations and constraints for group - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for group =#=#=#= Begin test: Recursively check locations and constraints for group (XML) =#=#=#= =#=#=#= End test: Recursively check locations and constraints for group (XML) - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for group (XML) =#=#=#= Begin test: Check locations and constraints for clone =#=#=#= Resources colocated with clone: * prim9 (score=INFINITY, id=colocation-prim9-clone-INFINITY) =#=#=#= End test: Check locations and constraints for clone - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for clone =#=#=#= Begin test: Check locations and constraints for clone (XML) =#=#=#= =#=#=#= End test: Check locations and constraints for clone (XML) - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for clone (XML) =#=#=#= Begin test: Recursively check locations and constraints for clone =#=#=#= Resources colocated with clone: * prim9 (score=INFINITY, id=colocation-prim9-clone-INFINITY) =#=#=#= End test: Recursively check locations and constraints for clone - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for clone =#=#=#= Begin test: Recursively check locations and constraints for clone (XML) =#=#=#= =#=#=#= End test: Recursively check locations and constraints for clone (XML) - OK (0) =#=#=#= * Passed: crm_resource - Recursively check locations and constraints for clone (XML) =#=#=#= Begin test: Check locations and constraints for group member (referring to group) =#=#=#= Resources colocated with group: * prim7 (score=INFINITY, id=colocation-prim7-group-INFINITY) =#=#=#= End test: Check locations and constraints for group member (referring to group) - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for group member (referring to group) =#=#=#= Begin test: Check locations and constraints for group member (without referring to group) =#=#=#= Resources colocated with gr2: * prim8 (score=INFINITY, id=colocation-prim8-gr2-INFINITY) =#=#=#= End test: Check locations and constraints for group member (without referring to group) - OK (0) =#=#=#= * Passed: crm_resource - Check locations and constraints for group member (without referring to group) =#=#=#= Begin test: Set a meta-attribute for primitive and resources colocated with it (XML) =#=#=#= =#=#=#= End test: Set a meta-attribute for primitive and resources colocated with it (XML) - OK (0) =#=#=#= * Passed: crm_resource - Set a meta-attribute for primitive and resources colocated with it (XML) =#=#=#= Begin test: Set a meta-attribute for group and resource colocated with it =#=#=#= Set 'group' option: id=group-meta_attributes-target-role set=group-meta_attributes name=target-role value=Stopped Set 'prim7' option: id=prim7-meta_attributes-target-role set=prim7-meta_attributes name=target-role value=Stopped =#=#=#= End test: Set a meta-attribute for group and resource colocated with it - OK (0) =#=#=#= * Passed: crm_resource - Set a meta-attribute for group and resource colocated with it =#=#=#= Begin test: Set a meta-attribute for clone and resource colocated with it (XML) =#=#=#= =#=#=#= End test: Set a meta-attribute for clone and resource colocated with it (XML) - OK (0) =#=#=#= * Passed: crm_resource - Set a meta-attribute for clone and resource colocated with it (XML) =#=#=#= Begin test: Show resource digests (XML) =#=#=#= =#=#=#= End test: Show resource digests (XML) - OK (0) =#=#=#= * Passed: crm_resource - Show resource digests (XML) =#=#=#= Begin test: Show resource digests with overrides =#=#=#= =#=#=#= End test: Show resource digests with overrides - OK (0) =#=#=#= * Passed: crm_resource - Show resource digests with overrides =#=#=#= Begin test: Show resource operations =#=#=#= rsc1 (ocf:pacemaker:Dummy): Started: rsc1_monitor_0 (node=node4, call=136, rc=7, exec=28ms): complete Fencing (stonith:fence_xvm): Started: Fencing_monitor_0 (node=node4, call=5, rc=7, exec=2ms): complete rsc1 (ocf:pacemaker:Dummy): Started: rsc1_monitor_0 (node=node2, call=101, rc=7, exec=45ms): complete Fencing (stonith:fence_xvm): Started: Fencing_monitor_0 (node=node2, call=5, rc=7, exec=4ms): complete Fencing (stonith:fence_xvm): Started: Fencing_monitor_0 (node=node3, call=5, rc=7, exec=24ms): complete rsc1 (ocf:pacemaker:Dummy): Started: rsc1_monitor_0 (node=node5, call=99, rc=193, exec=27ms): pending Fencing (stonith:fence_xvm): Started: Fencing_monitor_0 (node=node5, call=5, rc=7, exec=14ms): complete rsc1 (ocf:pacemaker:Dummy): Started: rsc1_start_0 (node=node1, call=104, rc=0, exec=22ms): complete rsc1 (ocf:pacemaker:Dummy): Started: rsc1_monitor_10000 (node=node1, call=106, rc=0, exec=20ms): complete Fencing (stonith:fence_xvm): Started: Fencing_start_0 (node=node1, call=10, rc=0, exec=59ms): complete Fencing (stonith:fence_xvm): Started: Fencing_monitor_120000 (node=node1, call=12, rc=0, exec=70ms): complete =#=#=#= End test: Show resource operations - OK (0) =#=#=#= * Passed: crm_resource - Show resource operations =#=#=#= Begin test: Show resource operations (XML) =#=#=#= =#=#=#= End test: Show resource operations (XML) - OK (0) =#=#=#= * Passed: crm_resource - Show resource operations (XML) =#=#=#= Begin test: List a promotable clone resource =#=#=#= resource promotable-clone is running on: cluster01 resource promotable-clone is running on: cluster02 Promoted =#=#=#= End test: List a promotable clone resource - OK (0) =#=#=#= * Passed: crm_resource - List a promotable clone resource =#=#=#= Begin test: List a promotable clone resource (XML) =#=#=#= cluster01 cluster02 =#=#=#= End test: List a promotable clone resource (XML) - OK (0) =#=#=#= * Passed: crm_resource - List a promotable clone resource (XML) =#=#=#= Begin test: List the primitive of a promotable clone resource =#=#=#= resource promotable-rsc is running on: cluster01 resource promotable-rsc is running on: cluster02 Promoted =#=#=#= End test: List the primitive of a promotable clone resource - OK (0) =#=#=#= * Passed: crm_resource - List the primitive of a promotable clone resource =#=#=#= Begin test: List the primitive of a promotable clone resource (XML) =#=#=#= cluster01 cluster02 =#=#=#= End test: List the primitive of a promotable clone resource (XML) - OK (0) =#=#=#= * Passed: crm_resource - List the primitive of a promotable clone resource (XML) =#=#=#= Begin test: List a single instance of a promotable clone resource =#=#=#= resource promotable-rsc:0 is running on: cluster02 Promoted =#=#=#= End test: List a single instance of a promotable clone resource - OK (0) =#=#=#= * Passed: crm_resource - List a single instance of a promotable clone resource =#=#=#= Begin test: List a single instance of a promotable clone resource (XML) =#=#=#= cluster02 =#=#=#= End test: List a single instance of a promotable clone resource (XML) - OK (0) =#=#=#= * Passed: crm_resource - List a single instance of a promotable clone resource (XML) =#=#=#= Begin test: List another instance of a promotable clone resource =#=#=#= resource promotable-rsc:1 is running on: cluster01 =#=#=#= End test: List another instance of a promotable clone resource - OK (0) =#=#=#= * Passed: crm_resource - List another instance of a promotable clone resource =#=#=#= Begin test: List another instance of a promotable clone resource (XML) =#=#=#= cluster01 =#=#=#= End test: List another instance of a promotable clone resource (XML) - OK (0) =#=#=#= * Passed: crm_resource - List another instance of a promotable clone resource (XML) =#=#=#= Begin test: Try to move an instance of a cloned resource =#=#=#= crm_resource: Cannot operate on clone resource instance 'promotable-rsc:0' Error performing operation: Invalid parameter =#=#=#= End test: Try to move an instance of a cloned resource - Invalid parameter (2) =#=#=#= * Passed: crm_resource - Try to move an instance of a cloned resource =#=#=#= Begin test: Check that CIB_file="-" works - crm_resource (XML) =#=#=#= =#=#=#= End test: Check that CIB_file="-" works - crm_resource (XML) - OK (0) =#=#=#= * Passed: crm_resource - Check that CIB_file="-" works - crm_resource (XML) diff --git a/cts/cli/regression.crm_shadow.exp b/cts/cli/regression.crm_shadow.exp index 63869d7b60..ecf9541de0 100644 --- a/cts/cli/regression.crm_shadow.exp +++ b/cts/cli/regression.crm_shadow.exp @@ -1,1593 +1,1593 @@ =#=#=#= Begin test: Get active shadow instance (no active instance) =#=#=#= crm_shadow: No active shadow configuration defined =#=#=#= End test: Get active shadow instance (no active instance) - No such object (105) =#=#=#= * Passed: crm_shadow - Get active shadow instance (no active instance) =#=#=#= Begin test: Get active shadow instance (no active instance) (XML) =#=#=#= crm_shadow: No active shadow configuration defined =#=#=#= End test: Get active shadow instance (no active instance) (XML) - No such object (105) =#=#=#= * Passed: crm_shadow - Get active shadow instance (no active instance) (XML) =#=#=#= Begin test: Get active shadow instance's file name (no active instance) =#=#=#= crm_shadow: No active shadow configuration defined =#=#=#= End test: Get active shadow instance's file name (no active instance) - No such object (105) =#=#=#= * Passed: crm_shadow - Get active shadow instance's file name (no active instance) =#=#=#= Begin test: Get active shadow instance's file name (no active instance) (XML) =#=#=#= crm_shadow: No active shadow configuration defined =#=#=#= End test: Get active shadow instance's file name (no active instance) (XML) - No such object (105) =#=#=#= * Passed: crm_shadow - Get active shadow instance's file name (no active instance) (XML) =#=#=#= Begin test: Get active shadow instance's contents (no active instance) =#=#=#= crm_shadow: No active shadow configuration defined =#=#=#= End test: Get active shadow instance's contents (no active instance) - No such object (105) =#=#=#= * Passed: crm_shadow - Get active shadow instance's contents (no active instance) =#=#=#= Begin test: Get active shadow instance's contents (no active instance) (XML) =#=#=#= crm_shadow: No active shadow configuration defined =#=#=#= End test: Get active shadow instance's contents (no active instance) (XML) - No such object (105) =#=#=#= * Passed: crm_shadow - Get active shadow instance's contents (no active instance) (XML) =#=#=#= Begin test: Get active shadow instance's diff (no active instance) =#=#=#= crm_shadow: No active shadow configuration defined =#=#=#= End test: Get active shadow instance's diff (no active instance) - No such object (105) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (no active instance) =#=#=#= Begin test: Get active shadow instance's diff (no active instance) (XML) =#=#=#= crm_shadow: No active shadow configuration defined =#=#=#= End test: Get active shadow instance's diff (no active instance) (XML) - No such object (105) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (no active instance) (XML) =#=#=#= Begin test: Create copied shadow instance =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create copied shadow instance - OK (0) =#=#=#= * Passed: crm_shadow - Create copied shadow instance =#=#=#= Begin test: Create copied shadow instance (XML) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create copied shadow instance (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Create copied shadow instance (XML) =#=#=#= Begin test: Get active shadow instance (copied) =#=#=#= cts-cli =#=#=#= End test: Get active shadow instance (copied) - OK (0) =#=#=#= * Passed: crm_shadow - Get active shadow instance (copied) =#=#=#= Begin test: Get active shadow instance (copied) (XML) =#=#=#= =#=#=#= End test: Get active shadow instance (copied) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Get active shadow instance (copied) (XML) =#=#=#= Begin test: Get active shadow instance's file name (copied) =#=#=#= /tmp/cts-cli.shadow/shadow.cts-cli =#=#=#= End test: Get active shadow instance's file name (copied) - OK (0) =#=#=#= * Passed: crm_shadow - Get active shadow instance's file name (copied) =#=#=#= Begin test: Get active shadow instance's file name (copied) (XML) =#=#=#= =#=#=#= End test: Get active shadow instance's file name (copied) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Get active shadow instance's file name (copied) (XML) =#=#=#= Begin test: Get active shadow instance's contents (copied) =#=#=#= - + =#=#=#= End test: Get active shadow instance's contents (copied) - OK (0) =#=#=#= * Passed: crm_shadow - Get active shadow instance's contents (copied) =#=#=#= Begin test: Get active shadow instance's contents (copied) (XML) =#=#=#= - + ]]> =#=#=#= End test: Get active shadow instance's contents (copied) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Get active shadow instance's contents (copied) (XML) =#=#=#= Begin test: Get active shadow instance's diff (copied) =#=#=#= =#=#=#= End test: Get active shadow instance's diff (copied) - OK (0) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (copied) =#=#=#= Begin test: Get active shadow instance's diff (copied) (XML) =#=#=#= =#=#=#= End test: Get active shadow instance's diff (copied) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (copied) (XML) =#=#=#= Begin test: Get active shadow instance's diff (after changes) =#=#=#= Diff: --- 1.1.173 2 Diff: +++ 1.4.1 (null) -- /cib/configuration/op_defaults + /cib: @epoch=4, @num_updates=1 + /cib/configuration/resources/primitive[@id='dummy']: @description=desc ++ /cib/configuration/resources: ++ /cib/status: =#=#=#= End test: Get active shadow instance's diff (after changes) - Error occurred (1) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (after changes) =#=#=#= Begin test: Get active shadow instance's diff (after changes) (XML) =#=#=#= - + ]]> =#=#=#= End test: Get active shadow instance's diff (after changes) (XML) - Error occurred (1) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (after changes) (XML) =#=#=#= Begin test: Commit shadow instance =#=#=#= crm_shadow: The commit command overwrites the active cluster configuration. To prevent accidental destruction of the cluster, the --force flag is required in order to proceed. =#=#=#= End test: Commit shadow instance - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Commit shadow instance =#=#=#= Begin test: Commit shadow instance (force) =#=#=#= =#=#=#= End test: Commit shadow instance (force) - OK (0) =#=#=#= * Passed: crm_shadow - Commit shadow instance (force) =#=#=#= Begin test: Get active shadow instance's diff (after commit) =#=#=#= Diff: --- 1.2.0 2 Diff: +++ 1.4.1 (null) + /cib: @epoch=4, @num_updates=1 ++ /cib/status: =#=#=#= End test: Get active shadow instance's diff (after commit) - Error occurred (1) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (after commit) =#=#=#= Begin test: Commit shadow instance (force) (all) =#=#=#= =#=#=#= End test: Commit shadow instance (force) (all) - OK (0) =#=#=#= * Passed: crm_shadow - Commit shadow instance (force) (all) =#=#=#= Begin test: Get active shadow instance's diff (after commit all) =#=#=#= Diff: --- 1.4.2 2 Diff: +++ 1.4.1 (null) + /cib: @num_updates=1 =#=#=#= End test: Get active shadow instance's diff (after commit all) - Error occurred (1) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (after commit all) =#=#=#= Begin test: Commit shadow instance (XML) =#=#=#= crm_shadow: The commit command overwrites the active cluster configuration. To prevent accidental destruction of the cluster, the --force flag is required in order to proceed. =#=#=#= End test: Commit shadow instance (XML) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Commit shadow instance (XML) =#=#=#= Begin test: Commit shadow instance (force) (XML) =#=#=#= =#=#=#= End test: Commit shadow instance (force) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Commit shadow instance (force) (XML) =#=#=#= Begin test: Get active shadow instance's diff (after commit) (XML) =#=#=#= - + ]]> =#=#=#= End test: Get active shadow instance's diff (after commit) (XML) - Error occurred (1) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (after commit) (XML) =#=#=#= Begin test: Commit shadow instance (force) (all) (XML) =#=#=#= =#=#=#= End test: Commit shadow instance (force) (all) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Commit shadow instance (force) (all) (XML) =#=#=#= Begin test: Get active shadow instance's diff (after commit all) (XML) =#=#=#= - + ]]> =#=#=#= End test: Get active shadow instance's diff (after commit all) (XML) - Error occurred (1) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (after commit all) (XML) =#=#=#= Begin test: Commit shadow instance (no active instance) =#=#=#= crm_shadow: The commit command overwrites the active cluster configuration. To prevent accidental destruction of the cluster, the --force flag is required in order to proceed. =#=#=#= End test: Commit shadow instance (no active instance) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Commit shadow instance (no active instance) =#=#=#= Begin test: Commit shadow instance (no active instance) (XML) =#=#=#= crm_shadow: The commit command overwrites the active cluster configuration. To prevent accidental destruction of the cluster, the --force flag is required in order to proceed. =#=#=#= End test: Commit shadow instance (no active instance) (XML) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Commit shadow instance (no active instance) (XML) =#=#=#= Begin test: Commit shadow instance (no active instance) (force) =#=#=#= =#=#=#= End test: Commit shadow instance (no active instance) (force) - OK (0) =#=#=#= * Passed: crm_shadow - Commit shadow instance (no active instance) (force) =#=#=#= Begin test: Commit shadow instance (no active instance) (force) (XML) =#=#=#= =#=#=#= End test: Commit shadow instance (no active instance) (force) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Commit shadow instance (no active instance) (force) (XML) =#=#=#= Begin test: Commit shadow instance (mismatch) =#=#=#= crm_shadow: The commit command overwrites the active cluster configuration. Additionally, the supplied shadow instance (cts-cli) is not the same as the active one (nonexistent_shadow). To prevent accidental destruction of the cluster, the --force flag is required in order to proceed. =#=#=#= End test: Commit shadow instance (mismatch) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Commit shadow instance (mismatch) =#=#=#= Begin test: Commit shadow instance (mismatch) (XML) =#=#=#= crm_shadow: The commit command overwrites the active cluster configuration. Additionally, the supplied shadow instance (cts-cli) is not the same as the active one (nonexistent_shadow). To prevent accidental destruction of the cluster, the --force flag is required in order to proceed. =#=#=#= End test: Commit shadow instance (mismatch) (XML) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Commit shadow instance (mismatch) (XML) =#=#=#= Begin test: Commit shadow instance (mismatch) (force) =#=#=#= =#=#=#= End test: Commit shadow instance (mismatch) (force) - OK (0) =#=#=#= * Passed: crm_shadow - Commit shadow instance (mismatch) (force) =#=#=#= Begin test: Commit shadow instance (mismatch) (force) (XML) =#=#=#= =#=#=#= End test: Commit shadow instance (mismatch) (force) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Commit shadow instance (mismatch) (force) (XML) =#=#=#= Begin test: Commit shadow instance (nonexistent shadow file) =#=#=#= crm_shadow: The commit command overwrites the active cluster configuration. To prevent accidental destruction of the cluster, the --force flag is required in order to proceed. =#=#=#= End test: Commit shadow instance (nonexistent shadow file) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Commit shadow instance (nonexistent shadow file) =#=#=#= Begin test: Commit shadow instance (nonexistent shadow file) (XML) =#=#=#= crm_shadow: The commit command overwrites the active cluster configuration. To prevent accidental destruction of the cluster, the --force flag is required in order to proceed. =#=#=#= End test: Commit shadow instance (nonexistent shadow file) (XML) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Commit shadow instance (nonexistent shadow file) (XML) =#=#=#= Begin test: Commit shadow instance (nonexistent shadow file) (force) =#=#=#= crm_shadow: Could not access shadow instance 'nonexistent_shadow': No such file or directory =#=#=#= End test: Commit shadow instance (nonexistent shadow file) (force) - No such object (105) =#=#=#= * Passed: crm_shadow - Commit shadow instance (nonexistent shadow file) (force) =#=#=#= Begin test: Commit shadow instance (nonexistent shadow file) (force) (XML) =#=#=#= crm_shadow: Could not access shadow instance 'nonexistent_shadow': No such file or directory =#=#=#= End test: Commit shadow instance (nonexistent shadow file) (force) (XML) - No such object (105) =#=#=#= * Passed: crm_shadow - Commit shadow instance (nonexistent shadow file) (force) (XML) =#=#=#= Begin test: Get active shadow instance's diff (nonexistent shadow file) =#=#=#= crm_shadow: Could not access shadow instance 'nonexistent_shadow': No such file or directory =#=#=#= End test: Get active shadow instance's diff (nonexistent shadow file) - No such object (105) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (nonexistent shadow file) =#=#=#= Begin test: Get active shadow instance's diff (nonexistent shadow file) (XML) =#=#=#= crm_shadow: Could not access shadow instance 'nonexistent_shadow': No such file or directory =#=#=#= End test: Get active shadow instance's diff (nonexistent shadow file) (XML) - No such object (105) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (nonexistent shadow file) (XML) =#=#=#= Begin test: Commit shadow instance (nonexistent CIB file) =#=#=#= crm_shadow: The commit command overwrites the active cluster configuration. To prevent accidental destruction of the cluster, the --force flag is required in order to proceed. =#=#=#= End test: Commit shadow instance (nonexistent CIB file) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Commit shadow instance (nonexistent CIB file) =#=#=#= Begin test: Commit shadow instance (nonexistent CIB file) (XML) =#=#=#= crm_shadow: The commit command overwrites the active cluster configuration. To prevent accidental destruction of the cluster, the --force flag is required in order to proceed. =#=#=#= End test: Commit shadow instance (nonexistent CIB file) (XML) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Commit shadow instance (nonexistent CIB file) (XML) =#=#=#= Begin test: Commit shadow instance (nonexistent CIB file) (force) =#=#=#= crm_shadow: Could not connect to CIB: No such device or address =#=#=#= End test: Commit shadow instance (nonexistent CIB file) (force) - No such object (105) =#=#=#= * Passed: crm_shadow - Commit shadow instance (nonexistent CIB file) (force) =#=#=#= Begin test: Commit shadow instance (nonexistent CIB file) (force) (XML) =#=#=#= crm_shadow: Could not connect to CIB: No such device or address =#=#=#= End test: Commit shadow instance (nonexistent CIB file) (force) (XML) - No such object (105) =#=#=#= * Passed: crm_shadow - Commit shadow instance (nonexistent CIB file) (force) (XML) =#=#=#= Begin test: Get active shadow instance's diff (nonexistent CIB file) =#=#=#= crm_shadow: Could not connect to CIB: No such device or address =#=#=#= End test: Get active shadow instance's diff (nonexistent CIB file) - No such object (105) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (nonexistent CIB file) =#=#=#= Begin test: Get active shadow instance's diff (nonexistent CIB file) (XML) =#=#=#= crm_shadow: Could not connect to CIB: No such device or address =#=#=#= End test: Get active shadow instance's diff (nonexistent CIB file) (XML) - No such object (105) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (nonexistent CIB file) (XML) =#=#=#= Begin test: Delete shadow instance =#=#=#= crm_shadow: The delete command removes the specified shadow file. To prevent accidental destruction of the shadow file, the --force flag is required in order to proceed. =#=#=#= End test: Delete shadow instance - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Delete shadow instance =#=#=#= Begin test: Delete shadow instance (force) =#=#=#= Remember to unset the CIB_shadow variable by entering the following into your shell: unset CIB_shadow =#=#=#= End test: Delete shadow instance (force) - OK (0) =#=#=#= * Passed: crm_shadow - Delete shadow instance (force) =#=#=#= Begin test: Delete shadow instance (XML) =#=#=#= crm_shadow: The delete command removes the specified shadow file. To prevent accidental destruction of the shadow file, the --force flag is required in order to proceed. =#=#=#= End test: Delete shadow instance (XML) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Delete shadow instance (XML) =#=#=#= Begin test: Delete shadow instance (force) (XML) =#=#=#= Remember to unset the CIB_shadow variable by entering the following into your shell: unset CIB_shadow =#=#=#= End test: Delete shadow instance (force) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Delete shadow instance (force) (XML) =#=#=#= Begin test: Delete shadow instance (no active instance) =#=#=#= crm_shadow: The delete command removes the specified shadow file. To prevent accidental destruction of the shadow file, the --force flag is required in order to proceed. =#=#=#= End test: Delete shadow instance (no active instance) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Delete shadow instance (no active instance) =#=#=#= Begin test: Delete shadow instance (no active instance) (force) =#=#=#= =#=#=#= End test: Delete shadow instance (no active instance) (force) - OK (0) =#=#=#= * Passed: crm_shadow - Delete shadow instance (no active instance) (force) =#=#=#= Begin test: Delete shadow instance (no active instance) (XML) =#=#=#= crm_shadow: The delete command removes the specified shadow file. To prevent accidental destruction of the shadow file, the --force flag is required in order to proceed. =#=#=#= End test: Delete shadow instance (no active instance) (XML) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Delete shadow instance (no active instance) (XML) =#=#=#= Begin test: Delete shadow instance (no active instance) (force) (XML) =#=#=#= =#=#=#= End test: Delete shadow instance (no active instance) (force) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Delete shadow instance (no active instance) (force) (XML) =#=#=#= Begin test: Delete shadow instance (mismatch) =#=#=#= crm_shadow: The delete command removes the specified shadow file. Additionally, the supplied shadow instance (cts-cli) is not the same as the active one (nonexistent_shadow). To prevent accidental destruction of the shadow file, the --force flag is required in order to proceed. =#=#=#= End test: Delete shadow instance (mismatch) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Delete shadow instance (mismatch) =#=#=#= Begin test: Delete shadow instance (mismatch) (force) =#=#=#= =#=#=#= End test: Delete shadow instance (mismatch) (force) - OK (0) =#=#=#= * Passed: crm_shadow - Delete shadow instance (mismatch) (force) =#=#=#= Begin test: Delete shadow instance (mismatch) (XML) =#=#=#= crm_shadow: The delete command removes the specified shadow file. Additionally, the supplied shadow instance (cts-cli) is not the same as the active one (nonexistent_shadow). To prevent accidental destruction of the shadow file, the --force flag is required in order to proceed. =#=#=#= End test: Delete shadow instance (mismatch) (XML) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Delete shadow instance (mismatch) (XML) =#=#=#= Begin test: Delete shadow instance (mismatch) (force) (XML) =#=#=#= =#=#=#= End test: Delete shadow instance (mismatch) (force) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Delete shadow instance (mismatch) (force) (XML) =#=#=#= Begin test: Delete shadow instance (nonexistent shadow file) =#=#=#= crm_shadow: The delete command removes the specified shadow file. To prevent accidental destruction of the shadow file, the --force flag is required in order to proceed. =#=#=#= End test: Delete shadow instance (nonexistent shadow file) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Delete shadow instance (nonexistent shadow file) =#=#=#= Begin test: Delete shadow instance (nonexistent shadow file) (force) =#=#=#= Remember to unset the CIB_shadow variable by entering the following into your shell: unset CIB_shadow =#=#=#= End test: Delete shadow instance (nonexistent shadow file) (force) - OK (0) =#=#=#= * Passed: crm_shadow - Delete shadow instance (nonexistent shadow file) (force) =#=#=#= Begin test: Delete shadow instance (nonexistent shadow file) (XML) =#=#=#= crm_shadow: The delete command removes the specified shadow file. To prevent accidental destruction of the shadow file, the --force flag is required in order to proceed. =#=#=#= End test: Delete shadow instance (nonexistent shadow file) (XML) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Delete shadow instance (nonexistent shadow file) (XML) =#=#=#= Begin test: Delete shadow instance (nonexistent shadow file) (force) (XML) =#=#=#= Remember to unset the CIB_shadow variable by entering the following into your shell: unset CIB_shadow =#=#=#= End test: Delete shadow instance (nonexistent shadow file) (force) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Delete shadow instance (nonexistent shadow file) (force) (XML) =#=#=#= Begin test: Delete shadow instance (nonexistent CIB file) =#=#=#= crm_shadow: The delete command removes the specified shadow file. To prevent accidental destruction of the shadow file, the --force flag is required in order to proceed. =#=#=#= End test: Delete shadow instance (nonexistent CIB file) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Delete shadow instance (nonexistent CIB file) =#=#=#= Begin test: Delete shadow instance (nonexistent CIB file) (force) =#=#=#= Remember to unset the CIB_shadow variable by entering the following into your shell: unset CIB_shadow =#=#=#= End test: Delete shadow instance (nonexistent CIB file) (force) - OK (0) =#=#=#= * Passed: crm_shadow - Delete shadow instance (nonexistent CIB file) (force) =#=#=#= Begin test: Delete shadow instance (nonexistent CIB file) (XML) =#=#=#= crm_shadow: The delete command removes the specified shadow file. To prevent accidental destruction of the shadow file, the --force flag is required in order to proceed. =#=#=#= End test: Delete shadow instance (nonexistent CIB file) (XML) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Delete shadow instance (nonexistent CIB file) (XML) =#=#=#= Begin test: Delete shadow instance (nonexistent CIB file) (force) (XML) =#=#=#= Remember to unset the CIB_shadow variable by entering the following into your shell: unset CIB_shadow =#=#=#= End test: Delete shadow instance (nonexistent CIB file) (force) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Delete shadow instance (nonexistent CIB file) (force) (XML) =#=#=#= Begin test: Create copied shadow instance (no active instance) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create copied shadow instance (no active instance) - OK (0) =#=#=#= * Passed: crm_shadow - Create copied shadow instance (no active instance) =#=#=#= Begin test: Create copied shadow instance (no active instance) (XML) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create copied shadow instance (no active instance) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Create copied shadow instance (no active instance) (XML) =#=#=#= Begin test: Create copied shadow instance (mismatch) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create copied shadow instance (mismatch) - OK (0) =#=#=#= * Passed: crm_shadow - Create copied shadow instance (mismatch) =#=#=#= Begin test: Create copied shadow instance (mismatch) (XML) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create copied shadow instance (mismatch) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Create copied shadow instance (mismatch) (XML) =#=#=#= Begin test: Create copied shadow instance (file already exists) =#=#=#= crm_shadow: A shadow instance 'cts-cli' already exists. To prevent accidental destruction of the shadow file, the --force flag is required in order to proceed. =#=#=#= End test: Create copied shadow instance (file already exists) - Cannot create output file (73) =#=#=#= * Passed: crm_shadow - Create copied shadow instance (file already exists) =#=#=#= Begin test: Create copied shadow instance (file already exists) (XML) =#=#=#= crm_shadow: A shadow instance 'cts-cli' already exists. To prevent accidental destruction of the shadow file, the --force flag is required in order to proceed. =#=#=#= End test: Create copied shadow instance (file already exists) (XML) - Cannot create output file (73) =#=#=#= * Passed: crm_shadow - Create copied shadow instance (file already exists) (XML) =#=#=#= Begin test: Create copied shadow instance (file already exists) (force) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create copied shadow instance (file already exists) (force) - OK (0) =#=#=#= * Passed: crm_shadow - Create copied shadow instance (file already exists) (force) =#=#=#= Begin test: Create copied shadow instance (file already exists) (force) (XML) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create copied shadow instance (file already exists) (force) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Create copied shadow instance (file already exists) (force) (XML) =#=#=#= Begin test: Create copied shadow instance (nonexistent CIB file) (force) =#=#=#= crm_shadow: Could not connect to CIB: No such device or address =#=#=#= End test: Create copied shadow instance (nonexistent CIB file) (force) - No such object (105) =#=#=#= * Passed: crm_shadow - Create copied shadow instance (nonexistent CIB file) (force) =#=#=#= Begin test: Create copied shadow instance (nonexistent CIB file) (force) (XML) =#=#=#= crm_shadow: Could not connect to CIB: No such device or address =#=#=#= End test: Create copied shadow instance (nonexistent CIB file) (force) (XML) - No such object (105) =#=#=#= * Passed: crm_shadow - Create copied shadow instance (nonexistent CIB file) (force) (XML) =#=#=#= Begin test: Create empty shadow instance =#=#=#= Created new pacemaker configuration A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create empty shadow instance - OK (0) =#=#=#= * Passed: crm_shadow - Create empty shadow instance =#=#=#= Begin test: Create empty shadow instance (XML) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create empty shadow instance (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Create empty shadow instance (XML) =#=#=#= Begin test: Create empty shadow instance (no active instance) =#=#=#= Created new pacemaker configuration A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create empty shadow instance (no active instance) - OK (0) =#=#=#= * Passed: crm_shadow - Create empty shadow instance (no active instance) =#=#=#= Begin test: Create empty shadow instance (no active instance) (XML) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create empty shadow instance (no active instance) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Create empty shadow instance (no active instance) (XML) =#=#=#= Begin test: Create empty shadow instance (mismatch) =#=#=#= Created new pacemaker configuration A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create empty shadow instance (mismatch) - OK (0) =#=#=#= * Passed: crm_shadow - Create empty shadow instance (mismatch) =#=#=#= Begin test: Create empty shadow instance (mismatch) (XML) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create empty shadow instance (mismatch) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Create empty shadow instance (mismatch) (XML) =#=#=#= Begin test: Create empty shadow instance (nonexistent CIB file) =#=#=#= Created new pacemaker configuration A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create empty shadow instance (nonexistent CIB file) - OK (0) =#=#=#= * Passed: crm_shadow - Create empty shadow instance (nonexistent CIB file) =#=#=#= Begin test: Create empty shadow instance (nonexistent CIB file) (XML) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create empty shadow instance (nonexistent CIB file) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Create empty shadow instance (nonexistent CIB file) (XML) =#=#=#= Begin test: Create empty shadow instance (file already exists) =#=#=#= crm_shadow: A shadow instance 'cts-cli' already exists. To prevent accidental destruction of the shadow file, the --force flag is required in order to proceed. =#=#=#= End test: Create empty shadow instance (file already exists) - Cannot create output file (73) =#=#=#= * Passed: crm_shadow - Create empty shadow instance (file already exists) =#=#=#= Begin test: Create empty shadow instance (file already exists) (XML) =#=#=#= crm_shadow: A shadow instance 'cts-cli' already exists. To prevent accidental destruction of the shadow file, the --force flag is required in order to proceed. =#=#=#= End test: Create empty shadow instance (file already exists) (XML) - Cannot create output file (73) =#=#=#= * Passed: crm_shadow - Create empty shadow instance (file already exists) (XML) =#=#=#= Begin test: Create empty shadow instance (file already exists) (force) =#=#=#= Created new pacemaker configuration A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create empty shadow instance (file already exists) (force) - OK (0) =#=#=#= * Passed: crm_shadow - Create empty shadow instance (file already exists) (force) =#=#=#= Begin test: Create empty shadow instance (file already exists) (force) (XML) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Create empty shadow instance (file already exists) (force) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Create empty shadow instance (file already exists) (force) (XML) =#=#=#= Begin test: Get active shadow instance's contents (empty CIB) =#=#=#= =#=#=#= End test: Get active shadow instance's contents (empty CIB) - OK (0) =#=#=#= * Passed: crm_shadow - Get active shadow instance's contents (empty CIB) =#=#=#= Begin test: Get active shadow instance's contents (empty CIB) (XML) =#=#=#= ]]> =#=#=#= End test: Get active shadow instance's contents (empty CIB) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Get active shadow instance's contents (empty CIB) (XML) =#=#=#= Begin test: Get active shadow instance's diff (empty CIB) =#=#=#= Diff: --- 1.1.173 2 Diff: +++ 0.1.0 (null) -- /cib/configuration/crm_config/cluster_property_set[@id='cib-bootstrap-options'] -- /cib/configuration/nodes/node[@id='1'] -- /cib/configuration/nodes/node[@id='2'] -- /cib/configuration/resources/clone[@id='ping-clone'] -- /cib/configuration/resources/primitive[@id='Fencing'] -- /cib/configuration/resources/primitive[@id='dummy'] -- /cib/configuration/resources/clone[@id='inactive-clone'] -- /cib/configuration/resources/group[@id='inactive-group'] -- /cib/configuration/resources/bundle[@id='httpd-bundle'] -- /cib/configuration/resources/group[@id='exim-group'] -- /cib/configuration/resources/clone[@id='mysql-clone-group'] -- /cib/configuration/resources/clone[@id='promotable-clone'] -- /cib/configuration/constraints/rsc_location[@id='not-on-cluster1'] -- /cib/configuration/constraints/rsc_location[@id='loc-promotable-clone'] -- /cib/configuration/tags -- /cib/configuration/op_defaults -- /cib/status/node_state[@id='2'] -- /cib/status/node_state[@id='1'] -- /cib/status/node_state[@id='httpd-bundle-0'] -- /cib/status/node_state[@id='httpd-bundle-1'] + /cib: @validate-with=pacemaker-X, @num_updates=0, @admin_epoch=0 -- /cib: @cib-last-written, @update-origin, @update-client, @update-user, @have-quorum, @dc-uuid =#=#=#= End test: Get active shadow instance's diff (empty CIB) - Error occurred (1) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (empty CIB) =#=#=#= Begin test: Get active shadow instance's diff (empty CIB) (XML) =#=#=#= ]]> =#=#=#= End test: Get active shadow instance's diff (empty CIB) (XML) - Error occurred (1) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (empty CIB) (XML) =#=#=#= Begin test: Reset shadow instance =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Reset shadow instance - OK (0) =#=#=#= * Passed: crm_shadow - Reset shadow instance =#=#=#= Begin test: Get active shadow instance's diff (after reset) =#=#=#= =#=#=#= End test: Get active shadow instance's diff (after reset) - OK (0) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (after reset) =#=#=#= Begin test: Reset shadow instance (XML) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Reset shadow instance (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Reset shadow instance (XML) =#=#=#= Begin test: Get active shadow instance's diff (after reset) (XML) =#=#=#= =#=#=#= End test: Get active shadow instance's diff (after reset) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Get active shadow instance's diff (after reset) (XML) =#=#=#= Begin test: Reset shadow instance (no active instance) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Reset shadow instance (no active instance) - OK (0) =#=#=#= * Passed: crm_shadow - Reset shadow instance (no active instance) =#=#=#= Begin test: Reset shadow instance (no active instance) (XML) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Reset shadow instance (no active instance) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Reset shadow instance (no active instance) (XML) =#=#=#= Begin test: Reset shadow instance (mismatch) =#=#=#= crm_shadow: The supplied shadow instance (cts-cli) is not the same as the active one (nonexistent_shadow). To prevent accidental destruction of the shadow file, the --force flag is required in order to proceed. =#=#=#= End test: Reset shadow instance (mismatch) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Reset shadow instance (mismatch) =#=#=#= Begin test: Reset shadow instance (mismatch) (force) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Reset shadow instance (mismatch) (force) - OK (0) =#=#=#= * Passed: crm_shadow - Reset shadow instance (mismatch) (force) =#=#=#= Begin test: Reset shadow instance (mismatch) (XML) =#=#=#= crm_shadow: The supplied shadow instance (cts-cli) is not the same as the active one (nonexistent_shadow). To prevent accidental destruction of the shadow file, the --force flag is required in order to proceed. =#=#=#= End test: Reset shadow instance (mismatch) (XML) - Incorrect usage (64) =#=#=#= * Passed: crm_shadow - Reset shadow instance (mismatch) (XML) =#=#=#= Begin test: Reset shadow instance (mismatch) (force) (XML) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Reset shadow instance (mismatch) (force) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Reset shadow instance (mismatch) (force) (XML) =#=#=#= Begin test: Reset shadow instance (nonexistent CIB file) =#=#=#= crm_shadow: Could not connect to CIB: No such device or address =#=#=#= End test: Reset shadow instance (nonexistent CIB file) - No such object (105) =#=#=#= * Passed: crm_shadow - Reset shadow instance (nonexistent CIB file) =#=#=#= Begin test: Reset shadow instance (nonexistent CIB file) (XML) =#=#=#= crm_shadow: Could not connect to CIB: No such device or address =#=#=#= End test: Reset shadow instance (nonexistent CIB file) (XML) - No such object (105) =#=#=#= * Passed: crm_shadow - Reset shadow instance (nonexistent CIB file) (XML) =#=#=#= Begin test: Reset shadow instance (nonexistent CIB file) (force) =#=#=#= crm_shadow: Could not connect to CIB: No such device or address =#=#=#= End test: Reset shadow instance (nonexistent CIB file) (force) - No such object (105) =#=#=#= * Passed: crm_shadow - Reset shadow instance (nonexistent CIB file) (force) =#=#=#= Begin test: Reset shadow instance (nonexistent CIB file) (force) (XML) =#=#=#= crm_shadow: Could not connect to CIB: No such device or address =#=#=#= End test: Reset shadow instance (nonexistent CIB file) (force) (XML) - No such object (105) =#=#=#= * Passed: crm_shadow - Reset shadow instance (nonexistent CIB file) (force) (XML) =#=#=#= Begin test: Reset shadow instance (nonexistent shadow file) =#=#=#= crm_shadow: Could not access shadow instance 'cts-cli': No such file or directory =#=#=#= End test: Reset shadow instance (nonexistent shadow file) - No such object (105) =#=#=#= * Passed: crm_shadow - Reset shadow instance (nonexistent shadow file) =#=#=#= Begin test: Reset shadow instance (nonexistent shadow file) (XML) =#=#=#= crm_shadow: Could not access shadow instance 'cts-cli': No such file or directory =#=#=#= End test: Reset shadow instance (nonexistent shadow file) (XML) - No such object (105) =#=#=#= * Passed: crm_shadow - Reset shadow instance (nonexistent shadow file) (XML) =#=#=#= Begin test: Reset shadow instance (nonexistent shadow file) (force) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Reset shadow instance (nonexistent shadow file) (force) - OK (0) =#=#=#= * Passed: crm_shadow - Reset shadow instance (nonexistent shadow file) (force) =#=#=#= Begin test: Reset shadow instance (nonexistent shadow file) (force) (XML) =#=#=#= A new shadow instance was created. To begin using it, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Reset shadow instance (nonexistent shadow file) (force) (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Reset shadow instance (nonexistent shadow file) (force) (XML) =#=#=#= Begin test: Switch to new shadow instance =#=#=#= To switch to the named shadow instance, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Switch to new shadow instance - OK (0) =#=#=#= * Passed: crm_shadow - Switch to new shadow instance =#=#=#= Begin test: Switch to new shadow instance (XML) =#=#=#= To switch to the named shadow instance, enter the following into your shell: export CIB_shadow=cts-cli =#=#=#= End test: Switch to new shadow instance (XML) - OK (0) =#=#=#= * Passed: crm_shadow - Switch to new shadow instance (XML) =#=#=#= Begin test: Switch to nonexistent shadow instance =#=#=#= crm_shadow: Could not access shadow instance 'cts-cli': No such file or directory =#=#=#= End test: Switch to nonexistent shadow instance - No such object (105) =#=#=#= * Passed: crm_shadow - Switch to nonexistent shadow instance =#=#=#= Begin test: Switch to nonexistent shadow instance (XML) =#=#=#= crm_shadow: Could not access shadow instance 'cts-cli': No such file or directory =#=#=#= End test: Switch to nonexistent shadow instance (XML) - No such object (105) =#=#=#= * Passed: crm_shadow - Switch to nonexistent shadow instance (XML) =#=#=#= Begin test: Switch to nonexistent shadow instance (force) =#=#=#= crm_shadow: Could not access shadow instance 'cts-cli': No such file or directory =#=#=#= End test: Switch to nonexistent shadow instance (force) - No such object (105) =#=#=#= * Passed: crm_shadow - Switch to nonexistent shadow instance (force) =#=#=#= Begin test: Switch to nonexistent shadow instance (force) (XML) =#=#=#= crm_shadow: Could not access shadow instance 'cts-cli': No such file or directory =#=#=#= End test: Switch to nonexistent shadow instance (force) (XML) - No such object (105) =#=#=#= * Passed: crm_shadow - Switch to nonexistent shadow instance (force) (XML) diff --git a/cts/cli/regression.validity.exp b/cts/cli/regression.validity.exp index e1bf3b41e3..704686bf81 100644 --- a/cts/cli/regression.validity.exp +++ b/cts/cli/regression.validity.exp @@ -1,427 +1,427 @@ =#=#=#= Begin test: Try to make resulting CIB invalid (enum violation) =#=#=#= Call failed: Update does not conform to the configured schema =#=#=#= Current cib after: Try to make resulting CIB invalid (enum violation) =#=#=#= =#=#=#= End test: Try to make resulting CIB invalid (enum violation) - Invalid configuration (78) =#=#=#= * Passed: cibadmin - Try to make resulting CIB invalid (enum violation) =#=#=#= Begin test: Run crm_simulate with invalid CIB (enum violation) =#=#=#= element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-1.2 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-1.3 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-2.0 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-2.1 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-2.2 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-2.3 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-2.4 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-2.5 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-2.6 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-2.7 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-2.8 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-2.9 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-2.10 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-3.0 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-3.1 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-3.2 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-3.3 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-3.4 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-3.5 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-3.6 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-3.7 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-3.8 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-3.9 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-3.10 does not validate element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order pcmk__update_schema debug: Schema pacemaker-4.0 does not validate Cannot upgrade configuration (claiming pacemaker-1.2 schema) to at least pacemaker-4.0 because it does not validate with any schema from pacemaker-1.2 to the latest =#=#=#= End test: Run crm_simulate with invalid CIB (enum violation) - Invalid configuration (78) =#=#=#= * Passed: crm_simulate - Run crm_simulate with invalid CIB (enum violation) =#=#=#= Begin test: Try to make resulting CIB invalid (unrecognized validate-with) =#=#=#= Call failed: Update does not conform to the configured schema =#=#=#= Current cib after: Try to make resulting CIB invalid (unrecognized validate-with) =#=#=#= =#=#=#= End test: Try to make resulting CIB invalid (unrecognized validate-with) - Invalid configuration (78) =#=#=#= * Passed: cibadmin - Try to make resulting CIB invalid (unrecognized validate-with) =#=#=#= Begin test: Run crm_simulate with invalid CIB (unrecognized validate-with) =#=#=#= element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-1.0 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-1.2 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-1.3 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-2.0 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-2.1 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-2.2 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-2.3 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-2.4 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-2.5 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-2.6 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-2.7 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-2.8 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-2.9 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-2.10 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-3.0 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-3.1 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-3.2 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-3.3 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-3.4 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-3.5 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-3.6 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-3.7 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-3.8 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-3.9 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-3.10 does not validate element cib: Relax-NG validity error : Invalid attribute validate-with for element cib pcmk__update_schema debug: Schema pacemaker-4.0 does not validate Cannot upgrade configuration (claiming pacemaker-9999.0 schema) to at least pacemaker-4.0 because it does not validate with any schema from the first to the latest =#=#=#= End test: Run crm_simulate with invalid CIB (unrecognized validate-with) - Invalid configuration (78) =#=#=#= * Passed: crm_simulate - Run crm_simulate with invalid CIB (unrecognized validate-with) =#=#=#= Begin test: Try to make resulting CIB invalid, but possibly recoverable (valid with X.Y+1) =#=#=#= Call failed: Update does not conform to the configured schema =#=#=#= Current cib after: Try to make resulting CIB invalid, but possibly recoverable (valid with X.Y+1) =#=#=#= =#=#=#= End test: Try to make resulting CIB invalid, but possibly recoverable (valid with X.Y+1) - Invalid configuration (78) =#=#=#= * Passed: cibadmin - Try to make resulting CIB invalid, but possibly recoverable (valid with X.Y+1) =#=#=#= Begin test: Run crm_simulate with invalid, but possibly recoverable CIB (valid with X.Y+1) =#=#=#= element tags: Relax-NG validity error : Element configuration has extra content: tags pcmk__update_schema debug: Schema pacemaker-1.2 does not validate pcmk__update_schema debug: Schema pacemaker-1.3 validates pcmk__update_schema debug: Schema pacemaker-2.0 validates pcmk__update_schema debug: Schema pacemaker-2.1 validates pcmk__update_schema debug: Schema pacemaker-2.2 validates pcmk__update_schema debug: Schema pacemaker-2.3 validates pcmk__update_schema debug: Schema pacemaker-2.4 validates pcmk__update_schema debug: Schema pacemaker-2.5 validates pcmk__update_schema debug: Schema pacemaker-2.6 validates pcmk__update_schema debug: Schema pacemaker-2.7 validates pcmk__update_schema debug: Schema pacemaker-2.8 validates pcmk__update_schema debug: Schema pacemaker-2.9 validates pcmk__update_schema debug: Schema pacemaker-2.10 validates pcmk__update_schema debug: Schema pacemaker-3.0 validates pcmk__update_schema debug: Schema pacemaker-3.1 validates pcmk__update_schema debug: Schema pacemaker-3.2 validates pcmk__update_schema debug: Schema pacemaker-3.3 validates pcmk__update_schema debug: Schema pacemaker-3.4 validates pcmk__update_schema debug: Schema pacemaker-3.5 validates pcmk__update_schema debug: Schema pacemaker-3.6 validates pcmk__update_schema debug: Schema pacemaker-3.7 validates pcmk__update_schema debug: Schema pacemaker-3.8 validates pcmk__update_schema debug: Schema pacemaker-3.9 validates pcmk__update_schema debug: Schema pacemaker-3.10 validates pcmk__update_schema debug: Schema pacemaker-4.0 validates pcmk__update_schema info: Transformed the configuration schema to pacemaker-4.0 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 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 Current cluster status: * Full List of Resources: * dummy1 (ocf:pacemaker:Dummy): Stopped * dummy2 (ocf:pacemaker:Dummy): Stopped Transition Summary: Executing Cluster Transition: Revised Cluster Status: * Full List of Resources: * dummy1 (ocf:pacemaker:Dummy): Stopped * dummy2 (ocf:pacemaker:Dummy): Stopped =#=#=#= End test: Run crm_simulate with invalid, but possibly recoverable CIB (valid with X.Y+1) - OK (0) =#=#=#= * Passed: crm_simulate - Run crm_simulate with invalid, but possibly recoverable CIB (valid with X.Y+1) =#=#=#= Begin test: Make resulting CIB valid, although without validate-with attribute =#=#=#= =#=#=#= Current cib after: Make resulting CIB valid, although without validate-with attribute =#=#=#= - + =#=#=#= End test: Make resulting CIB valid, although without validate-with attribute - OK (0) =#=#=#= * Passed: cibadmin - Make resulting CIB valid, although without validate-with attribute =#=#=#= Begin test: Run crm_simulate with valid CIB, but without validate-with attribute =#=#=#= Schema validation of configuration is disabled (support for validate-with set to "none" is deprecated and will be removed in a future release) 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 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 Current cluster status: * Full List of Resources: * dummy1 (ocf:pacemaker:Dummy): Stopped * dummy2 (ocf:pacemaker:Dummy): Stopped Transition Summary: Executing Cluster Transition: Revised Cluster Status: * Full List of Resources: * dummy1 (ocf:pacemaker:Dummy): Stopped * dummy2 (ocf:pacemaker:Dummy): Stopped =#=#=#= End test: Run crm_simulate with valid CIB, but without validate-with attribute - OK (0) =#=#=#= * Passed: crm_simulate - Run crm_simulate with valid CIB, but without validate-with attribute =#=#=#= Begin test: Make resulting CIB invalid, and without validate-with attribute =#=#=#= element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order =#=#=#= Current cib after: Make resulting CIB invalid, and without validate-with attribute =#=#=#= - + =#=#=#= End test: Make resulting CIB invalid, and without validate-with attribute - OK (0) =#=#=#= * Passed: cibadmin - Make resulting CIB invalid, and without validate-with attribute =#=#=#= Begin test: Run crm_simulate with invalid CIB, also without validate-with attribute =#=#=#= Schema validation of configuration is disabled (support for validate-with set to "none" is deprecated and will be removed in a future release) element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order 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 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 Current cluster status: * Full List of Resources: * dummy1 (ocf:pacemaker:Dummy): Stopped * dummy2 (ocf:pacemaker:Dummy): Stopped Transition Summary: Executing Cluster Transition: Revised Cluster Status: * Full List of Resources: * dummy1 (ocf:pacemaker:Dummy): Stopped * dummy2 (ocf:pacemaker:Dummy): Stopped =#=#=#= End test: Run crm_simulate with invalid CIB, also without validate-with attribute - OK (0) =#=#=#= * Passed: crm_simulate - Run crm_simulate with invalid CIB, also without validate-with attribute diff --git a/cts/cts-cli.in b/cts/cts-cli.in index 22d943476f..e9b1b7afc4 100644 --- a/cts/cts-cli.in +++ b/cts/cts-cli.in @@ -1,3428 +1,3427 @@ #!@PYTHON@ """Regression tests for Pacemaker's command line tools.""" # pylint doesn't like the module name "cts-cli" which is an invalid complaint for this file # but probably something we want to continue warning about elsewhere # pylint: disable=invalid-name # pacemaker imports need to come after we modify sys.path, which pylint will complain about. # pylint: disable=wrong-import-position # We know this is a very long file. # pylint: disable=too-many-lines __copyright__ = "Copyright 2024 the Pacemaker project contributors" __license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY" import argparse from contextlib import contextmanager from datetime import datetime, timedelta import fileinput from functools import partial from gettext import ngettext from multiprocessing import Pool, cpu_count import os import pathlib import re from shutil import copyfile import signal from string import Formatter import subprocess import sys from tempfile import NamedTemporaryFile, TemporaryDirectory, mkstemp import types # These imports allow running from a source checkout after running `make`. if os.path.exists("@abs_top_srcdir@/python"): sys.path.insert(0, "@abs_top_srcdir@/python") # pylint: disable=comparison-of-constants,comparison-with-itself,condition-evals-to-constant if os.path.exists("@abs_top_builddir@/python") and "@abs_top_builddir@" != "@abs_top_srcdir@": sys.path.insert(0, "@abs_top_builddir@/python") from pacemaker._cts.errors import XmlValidationError from pacemaker._cts.validate import validate from pacemaker.buildoptions import BuildOptions from pacemaker.exitstatus import ExitStatus # Individual tool tests are split out, but can also be accessed as a group with "tools" tools_tests = ["cibadmin", "crm_attribute", "crm_standby", "crm_resource", "crm_ticket", "crmadmin", "crm_shadow", "crm_verify"] # The default list of tests to run, in the order they should be run default_tests = ["access_render", "daemons", "dates", "error_codes"] + tools_tests + \ ["crm_mon", "acls", "validity", "upgrade", "rules", "feature_set"] other_tests = ["agents"] # The directory containing this program test_home = os.path.dirname(os.path.realpath(__file__)) # The name of the shadow CIB SHADOW_NAME = "cts-cli" # Arguments to pass to valgrind VALGRIND_ARGS = ["-q", "--gen-suppressions=all", "--show-reachable=no", "--leak-check=full", "--trace-children=no", "--time-stamp=yes", "--num-callers=20", "--suppressions=%s/valgrind-pcmk.suppressions" % test_home] class PluralFormatter(Formatter): """ Special string formatting class for selecting singular vs. plurals. Use like so: fmt = PluralFormatter() print(fmt.format("{0} {0}:plural,test,tests} succeeded", n_tests)) """ def format_field(self, value, format_spec): """Convert a value to a formatted representation.""" if format_spec.startswith("plural,"): eles = format_spec.split(',') if len(eles) == 2: singular = eles[1] plural = singular + "s" else: singular = eles[1] plural = eles[2] return ngettext(singular, plural, value) return super().format_field(value, format_spec) def apply_substitutions(s, extra=None): """Apply text substitutions to an input string and return it.""" substitutions = { "cts_cli_data": "%s/cli" % test_home, "shadow": SHADOW_NAME, "test_home": test_home, } if extra is not None: substitutions.update(extra) return s.format(**substitutions) def cleanup_shadow_dir(): """Remove any previously created shadow CIB directory.""" subprocess.run(["crm_shadow", "--force", "--delete", SHADOW_NAME], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True) def copy_existing_cib(existing): """ Generate a CIB by copying an existing one to a temporary location. This is suitable for use with the cib_gen= parameter to the TestGroup class. """ (fp, new) = mkstemp(prefix="cts-cli.cib.xml.") os.close(fp) copyfile(apply_substitutions(existing), new) return new def current_cib(): """Return the complete current CIB.""" with environ({"CIB_user": "root"}): return subprocess.check_output(["cibadmin", "-Q"], encoding="utf-8") def make_test_group(desc, cmd, classes, **kwargs): """ Create a TestGroup that replicates the same test for multiple classes. The given description, cmd, and kwargs will be passed as arguments to each Test subclass in the classes parameter. The resulting objects will then be added to a TestGroup and returned. The main purpose of this function is to be able to run the same test for both text and XML formats without having to duplicate everything. Thus, the cmd string may contain "{fmt}", which will have any --output-as= class variable substituted in. """ tests = [] for c in classes: obj = c(desc, apply_substitutions(cmd, extra={"fmt": c.format_args}), **kwargs) tests.append(obj) return TestGroup(tests) def create_shadow_cib(shadow_dir, create_empty=True, validate_with=None, valgrind=False): """ Create a shadow CIB file. Keyword arguments: create_empty -- If True, the shadow CIB will be empty. Otherwise, the shadow CIB will be a copy of the currently active cluster configuration. validate_with -- If not None, the schema version to validate the CIB against valgrind -- If True, run the create operation under valgrind """ args = ["crm_shadow", "--batch", "--force"] if create_empty: args += ["--create-empty", SHADOW_NAME] else: args += ["--create", SHADOW_NAME] if validate_with is not None: args += ["--validate-with", validate_with] if valgrind: args = ["valgrind"] + VALGRIND_ARGS + args os.environ["CIB_shadow_dir"] = shadow_dir os.environ["CIB_shadow"] = SHADOW_NAME subprocess.run(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True) delete_shadow_resource_defaults() def delete_shadow_resource_defaults(): """Clear out the rsc_defaults section from a shadow CIB file.""" # A newly created empty CIB might or might not have a rsc_defaults section # depending on whether the --with-resource-stickiness-default configure # option was used. To ensure regression tests behave the same either way, # delete any rsc_defaults after creating or erasing a CIB. subprocess.run(["cibadmin", "--delete", "--xml-text", ""], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True) # The above command might or might not bump the CIB version, so reset it # to ensure future changes result in the same version for comparison. reset_shadow_cib_version() def reset_shadow_cib_version(): """Set various version numbers in a shadow CIB file back to 0.""" with fileinput.input(files=[shadow_path()], inplace=True) as f: for line in f: line = re.sub('epoch="[0-9]*"', 'epoch="1"', line) line = re.sub('num_updates="[0-9]*"', 'num_updates="0"', line) line = re.sub('admin_epoch="[0-9]*"', 'admin_epoch="0"', line) print(line, end='') def run_cmd_list(cmds): """ Run one or more shell commands. cmds can be: * A string * A Python function * A list of the above Raises subprocess.CalledProcessError on error. """ if cmds is None: return if isinstance(cmds, (str, types.FunctionType)): cmds = [cmds] for c in cmds: if isinstance(c, types.FunctionType): c() else: subprocess.run(apply_substitutions(c), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, universal_newlines=True, check=True) def sanitize_output(s): """ Replace content in the output expected to change between test runs. This is stuff like version numbers, timestamps, source line numbers, build options, system names and messages, etc. """ # A list of tuples of regular expressions and their replacements. replacements = [ (r'Created new pacemaker-.* configuration', r'Created new pacemaker configuration'), (r'Device not configured', r'No such device or address'), (r'^Entity: line [0-9]+: ', r''), (r'Last change: .*', r'Last change:'), (r'Last updated: .*', r'Last updated:'), (r'^Migration will take effect until: .*', r'Migration will take effect until:'), (r'(\* Possible values.*: .*)\(default: [^)]*\)', r'\1(default: )'), (r"""-X '.*'""", r"""-X '...'"""), (r' api-version="[^"]*"', r' api-version="X"'), (r'\(apply_upgrade@.*\.c:[0-9]+\)', r'apply_upgrade'), (r'(', r'\1/>'), - (r'(', r'\1>'), + (r'( """ # Create a test CIB that has ACL roles basic_tests = [ Test("Configure some ACLs", "cibadmin -M -o acls -p", update_cib=True, stdin=acl_cib), Test("Enable ACLs", "crm_attribute -n enable-acl -v true", update_cib=True), # Run cibadmin --show-access on the test CIB as an ACL-restricted user Test("An instance of ACLs render (into color)", "cibadmin --force --show-access=color -Q --user tony"), Test("An instance of ACLs render (into namespacing)", "cibadmin --force --show-access=namespace -Q --user tony"), Test("An instance of ACLs render (into text)", "cibadmin --force --show-access=text -Q --user tony"), ] return [ ShadowTestGroup(basic_tests), ] class DaemonsRegressionTest(RegressionTest): """A class for testing command line options of pacemaker daemons.""" @property def name(self): """Return the name of this regression test.""" return "daemons" @property def tests(self): """A list of Test instances to be run as part of this regression test.""" return [ Test("Get CIB manager metadata", "pacemaker-based metadata"), Test("Get controller metadata", "pacemaker-controld metadata"), Test("Get fencer metadata", "pacemaker-fenced metadata"), Test("Get scheduler metadata", "pacemaker-schedulerd metadata"), ] class DatesRegressionTest(RegressionTest): """A class for testing handling of ISO8601 dates.""" @property def name(self): """Return the name of this regression test.""" return "dates" @property def tests(self): """A list of Test instances to be run as part of this regression test.""" invalid_periods = [ "", "2019-01-01 00:00:00Z", # Start with no end "2019-01-01 00:00:00Z/", # Start with only a trailing slash "PT2S/P1M", # Two durations "2019-13-01 00:00:00Z/P1M", # Out-of-range month "20191077T15/P1M", # Out-of-range day "2019-10-01T25:00:00Z/P1M", # Out-of-range hour "2019-10-01T24:00:01Z/P1M", # Hour 24 with anything but :00:00 "PT5H/20191001T007000Z", # Out-of-range minute "2019-10-01 00:00:80Z/P1M", # Out-of-range second "2019-10-01 00:00:10 +25:00/P1M", # Out-of-range offset hour "20191001T000010 -00:61/P1M", # Out-of-range offset minute "P1Y/2019-02-29 00:00:00Z", # Feb. 29 in non-leap-year "2019-01-01 00:00:00Z/P", # Duration with no values "P1Z/2019-02-20 00:00:00Z", # Invalid duration unit "P1YM/2019-02-20 00:00:00Z", # No number for duration unit ] # Ensure invalid period specifications are rejected invalid_period_tests = [] for p in invalid_periods: invalid_period_tests.append(Test("Invalid period - [%s]" % p, "iso8601 -p '%s'" % p, expected_rc=ExitStatus.INVALID_PARAM)) year_tests = [] for y in ["06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "40"]: year_tests.extend([ Test("20%s-W01-7" % y, "iso8601 -d '20%s-W01-7 00Z'" % y), Test("20%s-W01-7 - round-trip" % y, "iso8601 -d '20%s-W01-7 00Z' -W -E '20%s-W01-7 00:00:00Z'" % (y, y)), Test("20%s-W01-1" % y, "iso8601 -d '20%s-W01-1 00Z'" % y), Test("20%s-W01-1 - round-trip" % y, "iso8601 -d '20%s-W01-1 00Z' -W -E '20%s-W01-1 00:00:00Z'" % (y, y)) ]) return invalid_period_tests + [ make_test_group("'2005-040/2005-043' period", "iso8601 {fmt} -p '2005-040/2005-043'", [Test, ValidatingTest]), Test("2014-01-01 00:30:00 - 1 Hour", "iso8601 -d '2014-01-01 00:30:00Z' -D P-1H -E '2013-12-31 23:30:00Z'"), Test("Valid date - Feb 29 in leap year", "iso8601 -d '2020-02-29 00:00:00Z' -E '2020-02-29 00:00:00Z'"), Test("Valid date - using 'T' and offset", "iso8601 -d '20191201T131211 -05:00' -E '2019-12-01 18:12:11Z'"), Test("24:00:00 equivalent to 00:00:00 of next day", "iso8601 -d '2019-12-31 24:00:00Z' -E '2020-01-01 00:00:00Z'"), ] + year_tests + [ make_test_group("2009-W53-07", "iso8601 {fmt} -d '2009-W53-7 00:00:00Z' -W -E '2009-W53-7 00:00:00Z'", [Test, ValidatingTest]), Test("epoch + 2 Years 5 Months 6 Minutes", "iso8601 -d 'epoch' -D P2Y5MT6M -E '1972-06-01 00:06:00Z'"), Test("2009-01-31 + 1 Month", "iso8601 -d '20090131T000000Z' -D P1M -E '2009-02-28 00:00:00Z'"), Test("2009-01-31 + 2 Months", "iso8601 -d '2009-01-31 00:00:00Z' -D P2M -E '2009-03-31 00:00:00Z'"), Test("2009-01-31 + 3 Months", "iso8601 -d '2009-01-31 00:00:00Z' -D P3M -E '2009-04-30 00:00:00Z'"), make_test_group("2009-03-31 - 1 Month", "iso8601 {fmt} -d '2009-03-31 01:00:00 +01:00' -D P-1M -E '2009-02-28 00:00:00Z'", [Test, ValidatingTest]), make_test_group("2038-01-01 + 3 Months", "iso8601 {fmt} -d '2038-01-01 00:00:00Z' -D P3M -E '2038-04-01 00:00:00Z'", [Test, ValidatingTest]), ] class ErrorCodeRegressionTest(RegressionTest): """A class for testing error code reporting.""" @property def name(self): """Return the name of this regression test.""" return "error_codes" @property def tests(self): """A list of Test instances to be run as part of this regression test.""" # Legacy return codes # # Don't test unknown legacy code. FreeBSD includes a colon in strerror(), # while other distros do not. legacy_tests = [ make_test_group("Get legacy return code", "crm_error {fmt} 201", [Test, ValidatingTest]), make_test_group("Get legacy return code (with name)", "crm_error -n {fmt} 201", [Test, ValidatingTest]), make_test_group("Get multiple legacy return codes", "crm_error {fmt} 201 202", [Test, ValidatingTest]), make_test_group("Get multiple legacy return codes (with names)", "crm_error -n {fmt} 201 202", [Test, ValidatingTest]), # We can only rely on our custom codes, so we'll spot-check codes 201-209 Test("List legacy return codes (spot check)", "crm_error -l | grep 20[1-9]"), ValidatingTest("List legacy return codes (spot check)", "crm_error -l --output-as=xml | grep -Ev '&1"), Test("Require --force for CIB erasure", "cibadmin -E", expected_rc=ExitStatus.UNSAFE, update_cib=True), Test("Allow CIB erasure with --force", "cibadmin -E --force"), # Verify the output after erasure Test("Query CIB", "cibadmin -Q", setup=delete_shadow_resource_defaults, update_cib=True), ] # Add some stuff to the empty CIB so we know that erasing it did something. basic_tests_setup = [ """cibadmin -C -o nodes --xml-text ''""", """cibadmin -C -o crm_config --xml-text ''""", """cibadmin -C -o resources --xml-text ''""" ] return [ ShadowTestGroup(basic_tests, setup=basic_tests_setup), ] class CrmAttributeRegressionTest(RegressionTest): """A class for testing crm_attribute.""" @property def name(self): """Return the name of this regression test.""" return "crm_attribute" @property def tests(self): """A list of Test instances to be run as part of this regression test.""" options_tests = [ make_test_group("List all available options (invalid type)", "crm_attribute --list-options=asdf {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.USAGE), make_test_group("List non-advanced cluster options", "crm_attribute --list-options=cluster {fmt}", [Test, ValidatingTest]), make_test_group("List all available cluster options", "crm_attribute --list-options=cluster --all {fmt}", [Test, ValidatingTest]), Test("Return usage error if both -p and OCF_RESOURCE_INSTANCE are empty strings", "crm_attribute -N cluster01 -p '' -G", expected_rc=ExitStatus.USAGE), ] value_update_tests = [ Test("Query the value of an attribute that does not exist", "crm_attribute -n ABCD --query --quiet", expected_rc=ExitStatus.NOSUCH), Test("Configure something before erasing", "crm_attribute -n test_attr -v 5", update_cib=True), Test("Test '++' XML attribute update syntax", """cibadmin -M --score --xml-text=''""", update_cib=True), Test("Test '+=' XML attribute update syntax", """cibadmin -M --score --xml-text=''""", update_cib=True), make_test_group("Test '++' nvpair value update syntax", "crm_attribute -n test_attr -v 'value++' --score {fmt}", [Test, ValidatingTest], update_cib=True), make_test_group("Test '+=' nvpair value update syntax", "crm_attribute -n test_attr -v 'value+=2' --score {fmt}", [Test, ValidatingTest], update_cib=True), Test("Test '++' XML attribute update syntax (--score not set)", """cibadmin -M --xml-text=''""", update_cib=True), Test("Test '+=' XML attribute update syntax (--score not set)", """cibadmin -M --xml-text=''""", update_cib=True), make_test_group("Test '++' nvpair value update syntax (--score not set)", "crm_attribute -n test_attr -v 'value++' {fmt}", [Test, ValidatingTest], update_cib=True), make_test_group("Test '+=' nvpair value update syntax (--score not set)", "crm_attribute -n test_attr -v 'value+=2' {fmt}", [Test, ValidatingTest], update_cib=True), ] query_set_tests = [ Test("Set cluster option", "crm_attribute -n cluster-delay -v 60s", update_cib=True), Test("Query new cluster option", "cibadmin -Q -o crm_config | grep cib-bootstrap-options-cluster-delay"), Test("Set no-quorum policy", "crm_attribute -n no-quorum-policy -v ignore", update_cib=True), Test("Delete nvpair", """cibadmin -D -o crm_config --xml-text ''""", update_cib=True), Test("Create operation should fail", """cibadmin -C -o crm_config --xml-text ''""", expected_rc=ExitStatus.EXISTS, update_cib=True), Test("Modify cluster options section", """cibadmin -M -o crm_config --xml-text ''""", update_cib=True), Test("Query updated cluster option", "cibadmin -Q -o crm_config | grep cib-bootstrap-options-cluster-delay", update_cib=True), Test("Set duplicate cluster option", "crm_attribute -n cluster-delay -v 40s -s duplicate", update_cib=True), Test("Setting multiply defined cluster option should fail", "crm_attribute -n cluster-delay -v 30s", expected_rc=ExitStatus.MULTIPLE, update_cib=True), Test("Set cluster option with -s", "crm_attribute -n cluster-delay -v 30s -s duplicate", update_cib=True), Test("Delete cluster option with -i", "crm_attribute -n cluster-delay -D -i cib-bootstrap-options-cluster-delay", update_cib=True), Test("Create node1 and bring it online", "crm_simulate --live-check --in-place --node-up=node1", update_cib=True), Test("Create node attribute", "crm_attribute -n ram -v 1024M -N node1 -t nodes", update_cib=True), Test("Query new node attribute", "cibadmin -Q -o nodes | grep node1-ram", update_cib=True), Test("Create second node attribute", "crm_attribute -n rattr -v XYZ -N node1 -t nodes", update_cib=True), Test("Query node attributes by pattern", "crm_attribute -t nodes -P 'ra.*' -N node1 --query"), Test("Update node attributes by pattern", "crm_attribute -t nodes -P 'rat.*' -N node1 -v 10", update_cib=True), Test("Delete node attributes by pattern", "crm_attribute -t nodes -P 'rat.*' -N node1 -D", update_cib=True), Test("Set a transient (fail-count) node attribute", "crm_attribute -n fail-count-foo -v 3 -N node1 -t status", update_cib=True), Test("Query a fail count", "crm_failcount --query -r foo -N node1", update_cib=True), Test("Show node attributes with crm_simulate", "crm_simulate --live-check --show-attrs"), Test("Set a second transient node attribute", "crm_attribute -n fail-count-bar -v 5 -N node1 -t status", update_cib=True), Test("Query transient node attributes by pattern", "crm_attribute -t status -P fail-count -N node1 --query"), Test("Update transient node attributes by pattern", "crm_attribute -t status -P fail-count -N node1 -v 10", update_cib=True), Test("Delete transient node attributes by pattern", "crm_attribute -t status -P fail-count -N node1 -D", update_cib=True), Test("crm_attribute given invalid delete usage", "crm_attribute -t nodes -N node1 -D", expected_rc=ExitStatus.USAGE), Test("Set a utilization node attribute", "crm_attribute -n cpu -v 1 -N node1 -z", update_cib=True), Test("Query utilization node attribute", "crm_attribute --query -n cpu -N node1 -z"), # This update will fail because it has version numbers Test("Replace operation should fail", """cibadmin -Q | sed -e 's/epoch="[^"]*"/epoch="1"/' | cibadmin -R -p""", expected_rc=ExitStatus.OLD), ] promotable_tests = [ make_test_group("Query a nonexistent promotable score attribute", "crm_attribute -N cluster01 -p promotable-rsc -G {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.NOSUCH), make_test_group("Delete a nonexistent promotable score attribute", "crm_attribute -N cluster01 -p promotable-rsc -D {fmt}", [Test, ValidatingTest]), make_test_group("Query after deleting a nonexistent promotable score attribute", "crm_attribute -N cluster01 -p promotable-rsc -G {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.NOSUCH), make_test_group("Update a nonexistent promotable score attribute", "crm_attribute -N cluster01 -p promotable-rsc -v 1 {fmt}", [Test, ValidatingTest]), make_test_group("Query after updating a nonexistent promotable score attribute", "crm_attribute -N cluster01 -p promotable-rsc -G {fmt}", [Test, ValidatingTest]), make_test_group("Update an existing promotable score attribute", "crm_attribute -N cluster01 -p promotable-rsc -v 5 {fmt}", [Test, ValidatingTest]), make_test_group("Query after updating an existing promotable score attribute", "crm_attribute -N cluster01 -p promotable-rsc -G {fmt}", [Test, ValidatingTest]), make_test_group("Delete an existing promotable score attribute", "crm_attribute -N cluster01 -p promotable-rsc -D {fmt}", [Test, ValidatingTest]), make_test_group("Query after deleting an existing promotable score attribute", "crm_attribute -N cluster01 -p promotable-rsc -G {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.NOSUCH), ] # Test for an issue with legacy command line parsing when the resource is # specified in the environment (CLBZ#5509) ocf_rsc_instance_tests = [ make_test_group("Update a promotable score attribute to -INFINITY", "crm_attribute -N cluster01 -p -v -INFINITY {fmt}", [Test, ValidatingTest], env={"OCF_RESOURCE_INSTANCE": "promotable-rsc"}), make_test_group("Query after updating a promotable score attribute to -INFINITY", "crm_attribute -N cluster01 -p -G {fmt}", [Test, ValidatingTest], env={"OCF_RESOURCE_INSTANCE": "promotable-rsc"}), Test("Try OCF_RESOURCE_INSTANCE if -p is specified with an empty string", "crm_attribute -N cluster01 -p '' -G", env={"OCF_RESOURCE_INSTANCE": "promotable-rsc"}), ] return options_tests + [ ShadowTestGroup(value_update_tests), ShadowTestGroup(query_set_tests), TestGroup(promotable_tests + ocf_rsc_instance_tests, env={"OCF_RESOURCE_INSTANCE": "promotable-rsc"}, cib_gen=partial(copy_existing_cib, "{cts_cli_data}/crm_mon.xml")), ] class CrmStandbyRegressionTest(RegressionTest): """A class for testing crm_standby.""" @property def name(self): """Return the name of this regression test.""" return "crm_standby" @property def tests(self): """A list of Test instances to be run as part of this regression test.""" basic_tests = [ Test("Default standby value", "crm_standby -N node1 -G"), Test("Set standby status", "crm_standby -N node1 -v true", update_cib=True), Test("Query standby value", "crm_standby -N node1 -G"), Test("Delete standby value", "crm_standby -N node1 -D", update_cib=True), ] return [ ShadowTestGroup(basic_tests, setup="""cibadmin -C -o nodes --xml-text ''"""), ] class CrmResourceRegressionTest(RegressionTest): """A class for testing crm_resource.""" @property def name(self): """Return the name of this regression test.""" return "crm_resource" @property def tests(self): """A list of Test instances to be run as part of this regression test.""" options_tests = [ Test("crm_resource run with extra arguments", "crm_resource foo bar", expected_rc=ExitStatus.USAGE), Test("List all available resource options (invalid type)", "crm_resource --list-options=asdf", expected_rc=ExitStatus.USAGE), Test("List all available resource options (invalid type)", "crm_resource --list-options=asdf --output-as=xml", expected_rc=ExitStatus.USAGE), make_test_group("List non-advanced primitive meta-attributes", "crm_resource --list-options=primitive {fmt}", [Test, ValidatingTest]), make_test_group("List all available primitive meta-attributes", "crm_resource --list-options=primitive --all {fmt}", [Test, ValidatingTest]), make_test_group("List non-advanced fencing parameters", "crm_resource --list-options=fencing {fmt}", [Test, ValidatingTest]), make_test_group("List all available fencing parameters", "crm_resource --list-options=fencing --all {fmt}", [Test, ValidatingTest]), ] basic_tests = [ Test("Create a resource", """cibadmin -C -o resources --xml-text ''""", update_cib=True), Test("crm_resource given both -r and resource config", "crm_resource -r xyz --class ocf --provider pacemaker --agent Dummy", expected_rc=ExitStatus.USAGE), Test("crm_resource given resource config with invalid action", "crm_resource --class ocf --provider pacemaker --agent Dummy -D", expected_rc=ExitStatus.USAGE), Test("Create a resource meta attribute", "crm_resource -r dummy --meta -p is-managed -v false", update_cib=True), Test("Query a resource meta attribute", "crm_resource -r dummy --meta -g is-managed", update_cib=True), Test("Remove a resource meta attribute", "crm_resource -r dummy --meta -d is-managed", update_cib=True), ValidatingTest("Create another resource meta attribute", "crm_resource -r dummy --meta -p target-role -v Stopped --output-as=xml"), ValidatingTest("Show why a resource is not running", "crm_resource -Y -r dummy --output-as=xml"), ValidatingTest("Remove another resource meta attribute", "crm_resource -r dummy --meta -d target-role --output-as=xml"), ValidatingTest("Get a non-existent attribute from a resource element", "crm_resource -r dummy --get-parameter nonexistent --element --output-as=xml"), make_test_group("Get a non-existent attribute from a resource element", "crm_resource -r dummy --get-parameter nonexistent --element {fmt}", [Test, ValidatingTest], update_cib=True), Test("Get an existent attribute from a resource element", "crm_resource -r dummy --get-parameter class --element", update_cib=True), ValidatingTest("Set a non-existent attribute for a resource element", "crm_resource -r dummy --set-parameter=description -v test_description --element --output-as=xml", update_cib=True), ValidatingTest("Set an existent attribute for a resource element", "crm_resource -r dummy --set-parameter=description -v test_description --element --output-as=xml", update_cib=True), ValidatingTest("Delete an existent attribute for a resource element", "crm_resource -r dummy -d description --element --output-as=xml", update_cib=True), ValidatingTest("Delete a non-existent attribute for a resource element", "crm_resource -r dummy -d description --element --output-as=xml", update_cib=True), Test("Set a non-existent attribute for a resource element", "crm_resource -r dummy --set-parameter=description -v test_description --element", update_cib=True), Test("Set an existent attribute for a resource element", "crm_resource -r dummy --set-parameter=description -v test_description --element", update_cib=True), Test("Delete an existent attribute for a resource element", "crm_resource -r dummy -d description --element", update_cib=True), Test("Delete a non-existent attribute for a resource element", "crm_resource -r dummy -d description --element", update_cib=True), Test("Create a resource attribute", "crm_resource -r dummy -p delay -v 10s", update_cib=True), make_test_group("List the configured resources", "crm_resource -L {fmt}", [Test, ValidatingTest], update_cib=True), Test("Implicitly list the configured resources", "crm_resource"), Test("List IDs of instantiated resources", "crm_resource -l"), make_test_group("Show XML configuration of resource", "crm_resource -q -r dummy {fmt}", [Test, ValidatingTest]), Test("Require a destination when migrating a resource that is stopped", "crm_resource -r dummy -M", update_cib=True, expected_rc=ExitStatus.USAGE), Test("Don't support migration to non-existent locations", "crm_resource -r dummy -M -N i.do.not.exist", update_cib=True, expected_rc=ExitStatus.NOSUCH), Test("Create a fencing resource", """cibadmin -C -o resources --xml-text ''""", update_cib=True), Test("Bring resources online", "crm_simulate --live-check --in-place", update_cib=True), Test("Try to move a resource to its existing location", "crm_resource -r dummy --move --node node1", update_cib=True, expected_rc=ExitStatus.EXISTS), Test("Try to move a resource that doesn't exist", "crm_resource -r xyz --move --node node1", expected_rc=ExitStatus.NOSUCH), Test("Move a resource from its existing location", "crm_resource -r dummy --move", update_cib=True), Test("Clear out constraints generated by --move", "crm_resource -r dummy --clear", update_cib=True), Test("Ban a resource on unknown node", "crm_resource -r dummy -B -N host1", expected_rc=ExitStatus.NOSUCH), Test("Create two more nodes and bring them online", "crm_simulate --live-check --in-place --node-up=node2 --node-up=node3", update_cib=True), Test("Ban dummy from node1", "crm_resource -r dummy -B -N node1", update_cib=True), Test("Show where a resource is running", "crm_resource -r dummy -W"), Test("Show constraints on a resource", "crm_resource -a -r dummy"), ValidatingTest("Ban dummy from node2", "crm_resource -r dummy -B -N node2 --output-as=xml", update_cib=True), Test("Relocate resources due to ban", "crm_simulate --live-check --in-place -S", update_cib=True), ValidatingTest("Move dummy to node1", "crm_resource -r dummy -M -N node1 --output-as=xml", update_cib=True), Test("Clear implicit constraints for dummy on node2", "crm_resource -r dummy -U -N node2", update_cib=True), Test("Drop the status section", "cibadmin -R -o status --xml-text ''"), Test("Create a clone", """cibadmin -C -o resources --xml-text ''"""), Test("Create a resource meta attribute", "crm_resource -r test-primitive --meta -p is-managed -v false", update_cib=True), Test("Create a resource meta attribute in the primitive", "crm_resource -r test-primitive --meta -p is-managed -v false --force", update_cib=True), Test("Update resource meta attribute with duplicates", "crm_resource -r test-clone --meta -p is-managed -v true", update_cib=True), Test("Update resource meta attribute with duplicates (force clone)", "crm_resource -r test-clone --meta -p is-managed -v true --force", update_cib=True), Test("Update child resource meta attribute with duplicates", "crm_resource -r test-primitive --meta -p is-managed -v false", update_cib=True), Test("Delete resource meta attribute with duplicates", "crm_resource -r test-clone --meta -d is-managed", update_cib=True), Test("Delete resource meta attribute in parent", "crm_resource -r test-primitive --meta -d is-managed", update_cib=True), Test("Create a resource meta attribute in the primitive", "crm_resource -r test-primitive --meta -p is-managed -v false --force", update_cib=True), Test("Update existing resource meta attribute", "crm_resource -r test-clone --meta -p is-managed -v true", update_cib=True), Test("Create a resource meta attribute in the parent", "crm_resource -r test-clone --meta -p is-managed -v true --force", update_cib=True), Test("Delete resource parent meta attribute (force)", "crm_resource -r test-clone --meta -d is-managed --force", update_cib=True), # Restore meta-attributes before running this test Test("Delete resource child meta attribute", "crm_resource -r test-primitive --meta -d is-managed", setup=["crm_resource -r test-primitive --meta -p is-managed -v true --force", "crm_resource -r test-clone --meta -p is-managed -v true --force"], update_cib=True), Test("Create the dummy-group resource group", """cibadmin -C -o resources --xml-text '""" """""" """""" """'""", update_cib=True), Test("Create a resource meta attribute in dummy1", "crm_resource -r dummy1 --meta -p is-managed -v true", update_cib=True), Test("Create a resource meta attribute in dummy-group", "crm_resource -r dummy-group --meta -p is-managed -v false", update_cib=True), Test("Delete the dummy-group resource group", "cibadmin -D -o resources --xml-text ''", update_cib=True), Test("Specify a lifetime when moving a resource", "crm_resource -r dummy --move --node node2 --lifetime=PT1H", update_cib=True), Test("Try to move a resource previously moved with a lifetime", "crm_resource -r dummy --move --node node1", update_cib=True), Test("Ban dummy from node1 for a short time", "crm_resource -r dummy -B -N node1 --lifetime=PT1S", update_cib=True), Test("Remove expired constraints", "sleep 2 && crm_resource --clear --expired", update_cib=True), # Clear has already been tested elsewhere, but we need to get rid of the # constraints so testing delete works. It won't delete if there's still # a reference to the resource somewhere. Test("Clear all implicit constraints for dummy", "crm_resource -r dummy -U", update_cib=True), Test("Set a node health strategy", "crm_attribute -n node-health-strategy -v migrate-on-red", update_cib=True), Test("Set a node health attribute", "crm_attribute -N node3 -n '#health-cts-cli' -v red", update_cib=True), ValidatingTest("Show why a resource is not running on an unhealthy node", "crm_resource -N node3 -Y -r dummy --output-as=xml"), Test("Delete a resource", "crm_resource -D -r dummy -t primitive", update_cib=True), ] constraint_tests = [] for rsc in ["prim1", "prim2", "prim3", "prim4", "prim5", "prim6", "prim7", "prim8", "prim9", "prim10", "prim11", "prim12", "prim13", "group", "clone"]: constraint_tests.extend([ make_test_group("Check locations and constraints for %s" % rsc, "crm_resource -a -r %s {fmt}" % rsc, [Test, ValidatingTest]), make_test_group("Recursively check locations and constraints for %s" % rsc, "crm_resource -A -r %s {fmt}" % rsc, [Test, ValidatingTest]), ]) constraint_tests.extend([ Test("Check locations and constraints for group member (referring to group)", "crm_resource -a -r gr2"), Test("Check locations and constraints for group member (without referring to group)", "crm_resource -a -r gr2 --force"), ]) colocation_tests = [ ValidatingTest("Set a meta-attribute for primitive and resources colocated with it", "crm_resource -r prim5 --meta --set-parameter=target-role -v Stopped --recursive --output-as=xml"), Test("Set a meta-attribute for group and resource colocated with it", "crm_resource -r group --meta --set-parameter=target-role -v Stopped --recursive"), ValidatingTest("Set a meta-attribute for clone and resource colocated with it", "crm_resource -r clone --meta --set-parameter=target-role -v Stopped --recursive --output-as=xml"), ] digest_tests = [ ValidatingTest("Show resource digests", "crm_resource --digests -r rsc1 -N node1 --output-as=xml"), Test("Show resource digests with overrides", "crm_resource --digests -r rsc1 -N node1 --output-as=xml CRM_meta_interval=10000 CRM_meta_timeout=20000"), make_test_group("Show resource operations", "crm_resource --list-operations {fmt}", [Test, ValidatingTest]), ] basic2_tests = [ make_test_group("List a promotable clone resource", "crm_resource --locate -r promotable-clone {fmt}", [Test, ValidatingTest]), make_test_group("List the primitive of a promotable clone resource", "crm_resource --locate -r promotable-rsc {fmt}", [Test, ValidatingTest]), make_test_group("List a single instance of a promotable clone resource", "crm_resource --locate -r promotable-rsc:0 {fmt}", [Test, ValidatingTest]), make_test_group("List another instance of a promotable clone resource", "crm_resource --locate -r promotable-rsc:1 {fmt}", [Test, ValidatingTest]), Test("Try to move an instance of a cloned resource", "crm_resource -r promotable-rsc:0 --move --node node1", expected_rc=ExitStatus.INVALID_PARAM), ] basic_tests_setup = [ "crm_attribute -n no-quorum-policy -v ignore", "crm_simulate --live-check --in-place --node-up=node1" ] return options_tests + [ ShadowTestGroup(basic_tests, setup=basic_tests_setup), TestGroup(constraint_tests, env={"CIB_file": "{cts_cli_data}/constraints.xml"}), TestGroup(colocation_tests, cib_gen=partial(copy_existing_cib, "{cts_cli_data}/constraints.xml")), TestGroup(digest_tests, env={"CIB_file": "{cts_cli_data}/crm_resource_digests.xml"}), TestGroup(basic2_tests, env={"CIB_file": "{cts_cli_data}/crm_mon.xml"}), ValidatingTest("Check that CIB_file=\"-\" works - crm_resource", "crm_resource --digests -r rsc1 -N node1 --output-as=xml", env={"CIB_file": "-"}, stdin=pathlib.Path(apply_substitutions("{cts_cli_data}/crm_resource_digests.xml"))), ] class CrmTicketRegressionTest(RegressionTest): """A class for testing crm_ticket.""" @property def name(self): """Return the name of this regression test.""" return "crm_ticket" @property def tests(self): """A list of Test instances to be run as part of this regression test.""" basic_tests = [ Test("Default ticket granted state", "crm_ticket -t ticketA -G granted -d false"), Test("Set ticket granted state", "crm_ticket -t ticketA -r --force", update_cib=True), make_test_group("List ticket IDs", "crm_ticket -w {fmt}", [Test, ValidatingTest]), make_test_group("Query ticket state", "crm_ticket -t ticketA -q {fmt}", [Test, ValidatingTest]), make_test_group("Query ticket granted state", "crm_ticket -t ticketA -G granted {fmt}", [Test, ValidatingTest]), Test("Delete ticket granted state", "crm_ticket -t ticketA -D granted --force", update_cib=True), Test("Make a ticket standby", "crm_ticket -t ticketA -s", update_cib=True), Test("Query ticket standby state", "crm_ticket -t ticketA -G standby"), Test("Activate a ticket", "crm_ticket -t ticketA -a", update_cib=True), make_test_group("List ticket details", "crm_ticket -L -t ticketA {fmt}", [Test, ValidatingTest]), Test("Add a second ticket", "crm_ticket -t ticketB -G granted -d false", update_cib=True), Test("Set second ticket granted state", "crm_ticket -t ticketB -r --force", update_cib=True), make_test_group("List tickets", "crm_ticket -l {fmt}", [Test, ValidatingTest]), Test("Delete second ticket", """cibadmin --delete --xml-text ''""", update_cib=True), Test("Delete ticket standby state", "crm_ticket -t ticketA -D standby", update_cib=True), Test("Add a constraint to a ticket", """cibadmin -C -o constraints --xml-text ''""", update_cib=True), make_test_group("Query ticket constraints", "crm_ticket -t ticketA -c {fmt}", [Test, ValidatingTest]), Test("Delete ticket constraint", """cibadmin --delete --xml-text ''""", update_cib=True), ] basic_tests_setup = [ """cibadmin -C -o crm_config --xml-text ''""", """cibadmin -C -o resources --xml-text ''""" ] return [ ShadowTestGroup(basic_tests, setup=basic_tests_setup), ] class CrmadminRegressionTest(RegressionTest): """A class for testing crmadmin.""" @property def name(self): """Return the name of this regression test.""" return "crmadmin" @property def tests(self): """A list of Test instances to be run as part of this regression test.""" basic_tests = [ make_test_group("List all nodes", "crmadmin -N {fmt}", [Test, ValidatingTest]), make_test_group("Minimally list all nodes", "crmadmin -N -q {fmt}", [Test, ValidatingTest]), Test("List all nodes as bash exports", "crmadmin -N -B"), make_test_group("List cluster nodes", "crmadmin -N cluster {fmt}", [Test, ValidatingTest]), make_test_group("List guest nodes", "crmadmin -N guest {fmt}", [Test, ValidatingTest]), make_test_group("List remote nodes", "crmadmin -N remote {fmt}", [Test, ValidatingTest]), make_test_group("List cluster,remote nodes", "crmadmin -N cluster,remote {fmt}", [Test, ValidatingTest]), make_test_group("List guest,remote nodes", "crmadmin -N guest,remote {fmt}", [Test, ValidatingTest]), ] return [ TestGroup(basic_tests, env={"CIB_file": "{cts_cli_data}/crmadmin-cluster-remote-guest-nodes.xml"}), Test("Check that CIB_file=\"-\" works", "crmadmin -N", env={"CIB_file": "-"}, stdin=pathlib.Path(apply_substitutions("{cts_cli_data}/crmadmin-cluster-remote-guest-nodes.xml"))), ] class CrmShadowRegressionTest(RegressionTest): """A class for testing crm_shadow.""" @property def name(self): """Return the name of this regression test.""" return "crm_shadow" @property def tests(self): """A list of Test instances to be run as part of this regression test.""" no_instance_tests = [ make_test_group("Get active shadow instance (no active instance)", "crm_shadow --which {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.NOSUCH), make_test_group("Get active shadow instance's file name (no active instance)", "crm_shadow --file {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.NOSUCH), make_test_group("Get active shadow instance's contents (no active instance)", "crm_shadow --display {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.NOSUCH), make_test_group("Get active shadow instance's diff (no active instance)", "crm_shadow --diff {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.NOSUCH), ] # Create new shadow instance based on active CIB # Don't use create_shadow_cib() here; test explicitly new_instance_tests = [ make_test_group("Create copied shadow instance", "crm_shadow --create {shadow} --batch {fmt}", [Test, ValidatingTest], setup="crm_shadow --delete {shadow} --force"), # Query shadow instance based on active CIB make_test_group("Get active shadow instance (copied)", "crm_shadow --which {fmt}", [Test, ValidatingTest]), make_test_group("Get active shadow instance's file name (copied)", "crm_shadow --file {fmt}", [Test, ValidatingTest]), make_test_group("Get active shadow instance's contents (copied)", "crm_shadow --display {fmt}", [Test, ValidatingTest]), make_test_group("Get active shadow instance's diff (copied)", "crm_shadow --diff {fmt}", [Test, ValidatingTest]), ] # Make some changes to the shadow file modify_cib = """export CIB_file=$(crm_shadow --file) && """ \ """cibadmin --modify --xml-text '' && """ \ """cibadmin --delete --xml-text '' && """ \ """cibadmin --create -o resources --xml-text '' && """ \ """cibadmin --create -o status --xml-text ''""" more_tests = [ # We can't use make_test_group() here because we only want to run # the modify_cib setup code once, and make_test_group will pass all # kwargs to every instance it creates. Test("Get active shadow instance's diff (after changes)", "crm_shadow --diff", setup=modify_cib, expected_rc=ExitStatus.ERROR), ValidatingTest("Get active shadow instance's diff (after changes)", "crm_shadow --diff --output-as=xml", expected_rc=ExitStatus.ERROR), TestGroup([ # Commit the modified shadow CIB to a temp active CIB file Test("Commit shadow instance", "crm_shadow --commit {shadow}", expected_rc=ExitStatus.USAGE), Test("Commit shadow instance (force)", "crm_shadow --commit {shadow} --force"), Test("Get active shadow instance's diff (after commit)", "crm_shadow --diff", expected_rc=ExitStatus.ERROR), Test("Commit shadow instance (force) (all)", "crm_shadow --commit {shadow} --force --all"), Test("Get active shadow instance's diff (after commit all)", "crm_shadow --diff", expected_rc=ExitStatus.ERROR), ], cib_gen=partial(copy_existing_cib, "{cts_cli_data}/crm_mon.xml")), TestGroup([ # Repeat sequence with XML output ValidatingTest("Commit shadow instance", "crm_shadow --commit {shadow} --output-as=xml", expected_rc=ExitStatus.USAGE), ValidatingTest("Commit shadow instance (force)", "crm_shadow --commit {shadow} --force --output-as=xml"), ValidatingTest("Get active shadow instance's diff (after commit)", "crm_shadow --diff --output-as=xml", expected_rc=ExitStatus.ERROR), ValidatingTest("Commit shadow instance (force) (all)", "crm_shadow --commit {shadow} --force --all --output-as=xml"), ValidatingTest("Get active shadow instance's diff (after commit all)", "crm_shadow --diff --output-as=xml", expected_rc=ExitStatus.ERROR), # Commit an inactive shadow instance with no active instance make_test_group("Commit shadow instance (no active instance)", "crm_shadow --commit {shadow} {fmt}", [Test, ValidatingTest], env={"CIB_shadow": None}, expected_rc=ExitStatus.USAGE), make_test_group("Commit shadow instance (no active instance) (force)", "crm_shadow --commit {shadow} --force {fmt}", [Test, ValidatingTest], env={"CIB_shadow": None}), # Commit an inactive shadow instance with an active instance make_test_group("Commit shadow instance (mismatch)", "crm_shadow --commit {shadow} {fmt}", [Test, ValidatingTest], env={"CIB_shadow": "nonexistent_shadow"}, expected_rc=ExitStatus.USAGE), make_test_group("Commit shadow instance (mismatch) (force)", "crm_shadow --commit {shadow} --force {fmt}", [Test, ValidatingTest], env={"CIB_shadow": "nonexistent_shadow"}), # Commit an active shadow instance whose shadow file is missing make_test_group("Commit shadow instance (nonexistent shadow file)", "crm_shadow --commit nonexistent_shadow {fmt}", [Test, ValidatingTest], env={"CIB_shadow": "nonexistent_shadow"}, expected_rc=ExitStatus.USAGE), make_test_group("Commit shadow instance (nonexistent shadow file) (force)", "crm_shadow --commit nonexistent_shadow --force {fmt}", [Test, ValidatingTest], env={"CIB_shadow": "nonexistent_shadow"}, expected_rc=ExitStatus.NOSUCH), make_test_group("Get active shadow instance's diff (nonexistent shadow file)", "crm_shadow --diff {fmt}", [Test, ValidatingTest], env={"CIB_shadow": "nonexistent_shadow"}, expected_rc=ExitStatus.NOSUCH), # Commit an active shadow instance when the CIB file is missing make_test_group("Commit shadow instance (nonexistent CIB file)", "crm_shadow --commit {shadow} {fmt}", [Test, ValidatingTest], env={"CIB_file": "{cts_cli_data}/nonexistent_cib.xml"}, expected_rc=ExitStatus.USAGE), make_test_group("Commit shadow instance (nonexistent CIB file) (force)", "crm_shadow --commit {shadow} --force {fmt}", [Test, ValidatingTest], env={"CIB_file": "{cts_cli_data}/nonexistent_cib.xml"}, expected_rc=ExitStatus.NOSUCH), make_test_group("Get active shadow instance's diff (nonexistent CIB file)", "crm_shadow --diff {fmt}", [Test, ValidatingTest], env={"CIB_file": "{cts_cli_data}/nonexistent_cib.xml"}, expected_rc=ExitStatus.NOSUCH), ], cib_gen=partial(copy_existing_cib, "{cts_cli_data}/crm_mon.xml")), ] delete_1_tests = [ # Delete an active shadow instance Test("Delete shadow instance", "crm_shadow --delete {shadow}", expected_rc=ExitStatus.USAGE), Test("Delete shadow instance (force)", "crm_shadow --delete {shadow} --force"), ShadowTestGroup([ ValidatingTest("Delete shadow instance", "crm_shadow --delete {shadow} --output-as=xml", expected_rc=ExitStatus.USAGE), ValidatingTest("Delete shadow instance (force)", "crm_shadow --delete {shadow} --force --output-as=xml"), ]) ] delete_2_tests = [ # Delete an inactive shadow instance with no active instance Test("Delete shadow instance (no active instance)", "crm_shadow --delete {shadow}", expected_rc=ExitStatus.USAGE), Test("Delete shadow instance (no active instance) (force)", "crm_shadow --delete {shadow} --force"), ] delete_3_tests = [ ValidatingTest("Delete shadow instance (no active instance)", "crm_shadow --delete {shadow} --output-as=xml", expected_rc=ExitStatus.USAGE), ValidatingTest("Delete shadow instance (no active instance) (force)", "crm_shadow --delete {shadow} --force --output-as=xml"), ] delete_4_tests = [ # Delete an inactive shadow instance with an active instance Test("Delete shadow instance (mismatch)", "crm_shadow --delete {shadow}", expected_rc=ExitStatus.USAGE), Test("Delete shadow instance (mismatch) (force)", "crm_shadow --delete {shadow} --force"), ] delete_5_tests = [ ValidatingTest("Delete shadow instance (mismatch)", "crm_shadow --delete {shadow} --output-as=xml", expected_rc=ExitStatus.USAGE), ValidatingTest("Delete shadow instance (mismatch) (force)", "crm_shadow --delete {shadow} --force --output-as=xml"), # Delete an active shadow instance whose shadow file is missing Test("Delete shadow instance (nonexistent shadow file)", "crm_shadow --delete nonexistent_shadow", expected_rc=ExitStatus.USAGE), Test("Delete shadow instance (nonexistent shadow file) (force)", "crm_shadow --delete nonexistent_shadow --force"), ValidatingTest("Delete shadow instance (nonexistent shadow file)", "crm_shadow --delete nonexistent_shadow --output-as=xml", expected_rc=ExitStatus.USAGE), ValidatingTest("Delete shadow instance (nonexistent shadow file) (force)", "crm_shadow --delete nonexistent_shadow --force --output-as=xml"), ] delete_6_tests = [ # Delete an active shadow instance when the CIB file is missing Test("Delete shadow instance (nonexistent CIB file)", "crm_shadow --delete {shadow}", expected_rc=ExitStatus.USAGE), Test("Delete shadow instance (nonexistent CIB file) (force)", "crm_shadow --delete {shadow} --force"), ] delete_7_tests = [ ValidatingTest("Delete shadow instance (nonexistent CIB file)", "crm_shadow --delete {shadow} --output-as=xml", expected_rc=ExitStatus.USAGE), ValidatingTest("Delete shadow instance (nonexistent CIB file) (force)", "crm_shadow --delete {shadow} --force --output-as=xml"), ] create_1_tests = [ # Create new shadow instance based on active CIB with no instance active make_test_group("Create copied shadow instance (no active instance)", "crm_shadow --create {shadow} --batch {fmt}", [Test, ValidatingTest], setup="crm_shadow --delete {shadow} --force", env={"CIB_shadow": None}), # Create new shadow instance based on active CIB with other instance active make_test_group("Create copied shadow instance (mismatch)", "crm_shadow --create {shadow} --batch {fmt}", [Test, ValidatingTest], setup="crm_shadow --delete {shadow} --force", env={"CIB_shadow": "nonexistent_shadow"}), # Create new shadow instance based on CIB (shadow file already exists) make_test_group("Create copied shadow instance (file already exists)", "crm_shadow --create {shadow} --batch {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.CANTCREAT), make_test_group("Create copied shadow instance (file already exists) (force)", "crm_shadow --create {shadow} --batch --force {fmt}", [Test, ValidatingTest]), # Create new shadow instance based on active CIB when the CIB file is missing make_test_group("Create copied shadow instance (nonexistent CIB file) (force)", "crm_shadow --create {shadow} --batch --force {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.NOSUCH, setup="crm_shadow --delete {shadow} --force", env={"CIB_file": "{cts_cli_data}/nonexistent_cib.xml"}), ] create_2_tests = [ # Create new empty shadow instance make_test_group("Create empty shadow instance", "crm_shadow --create-empty {shadow} --batch {fmt}", [Test, ValidatingTest], setup="crm_shadow --delete {shadow} --force"), # Create empty shadow instance with no active instance make_test_group("Create empty shadow instance (no active instance)", "crm_shadow --create-empty {shadow} --batch {fmt}", [Test, ValidatingTest], setup="crm_shadow --delete {shadow} --force", env={"CIB_shadow": None}), # Create empty shadow instance with other instance active make_test_group("Create empty shadow instance (mismatch)", "crm_shadow --create-empty {shadow} --batch {fmt}", [Test, ValidatingTest], setup="crm_shadow --delete {shadow} --force", env={"CIB_shadow": "nonexistent_shadow"}), # Create empty shadow instance when the CIB file is missing make_test_group("Create empty shadow instance (nonexistent CIB file)", "crm_shadow --create-empty {shadow} --batch {fmt}", [Test, ValidatingTest], setup="crm_shadow --delete {shadow} --force", env={"CIB_file": "{cts_cli_data}/nonexistent_cib.xml"}), # Create empty shadow instance (shadow file already exists) make_test_group("Create empty shadow instance (file already exists)", "crm_shadow --create-empty {shadow} --batch {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.CANTCREAT), make_test_group("Create empty shadow instance (file already exists) (force)", "crm_shadow --create-empty {shadow} --batch --force {fmt}", [Test, ValidatingTest]), # Query shadow instance with an empty CIB. # --which and --file queries were done earlier. TestGroup([ make_test_group("Get active shadow instance's contents (empty CIB)", "crm_shadow --display {fmt}", [Test, ValidatingTest]), make_test_group("Get active shadow instance's diff (empty CIB)", "crm_shadow --diff {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.ERROR), ], setup=delete_shadow_resource_defaults), # Reset shadow instance (overwrite existing shadow file based on active CIB) Test("Reset shadow instance", "crm_shadow --reset {shadow} --batch"), Test("Get active shadow instance's diff (after reset)", "crm_shadow --diff"), ] reset_1_tests = [ ValidatingTest("Reset shadow instance", "crm_shadow --reset {shadow} --batch --output-as=xml"), ValidatingTest("Get active shadow instance's diff (after reset)", "crm_shadow --diff --output-as=xml"), # Reset an inactive shadow instance with no active instance Test("Reset shadow instance (no active instance)", "crm_shadow --reset {shadow} --batch", env={"CIB_shadow": None}), ] reset_2_tests = [ ValidatingTest("Reset shadow instance (no active instance)", "crm_shadow --reset {shadow} --batch --output-as=xml"), # Reset an inactive shadow instance with an active instance Test("Reset shadow instance (mismatch)", "crm_shadow --reset {shadow} --batch", env={"CIB_shadow": "nonexistent_shadow"}, expected_rc=ExitStatus.USAGE), Test("Reset shadow instance (mismatch) (force)", "crm_shadow --reset {shadow} --batch --force", env={"CIB_shadow": "nonexistent_shadow"}), ] reset_3_tests = [ ValidatingTest("Reset shadow instance (mismatch)", "crm_shadow --reset {shadow} --batch --output-as=xml", expected_rc=ExitStatus.USAGE), ValidatingTest("Reset shadow instance (mismatch) (force)", "crm_shadow --reset {shadow} --batch --force --output-as=xml"), ] reset_4_tests = [ # Reset an active shadow instance when the CIB file is missing make_test_group("Reset shadow instance (nonexistent CIB file)", "crm_shadow --reset {shadow} --batch {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.NOSUCH), make_test_group("Reset shadow instance (nonexistent CIB file) (force)", "crm_shadow --reset {shadow} --batch --force {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.NOSUCH), # Reset an active shadow instance whose shadow file is missing TestGroup([ make_test_group("Reset shadow instance (nonexistent shadow file)", "crm_shadow --reset {shadow} --batch {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.NOSUCH), ], env={"CIB_file": "{cts_cli_data}/crm_mon.xml"}, setup="crm_shadow --delete {shadow} --force"), TestGroup([ make_test_group("Reset shadow instance (nonexistent shadow file) (force)", "crm_shadow --reset {shadow} --batch --force {fmt}", [Test, ValidatingTest]), ], env={"CIB_file": "{cts_cli_data}/crm_mon.xml"}, setup="crm_shadow --delete {shadow} --force"), ] # Switch shadow instances switch_tests = [ make_test_group("Switch to new shadow instance", "crm_shadow --switch {shadow} --batch {fmt}", [Test, ValidatingTest]), TestGroup([ make_test_group("Switch to nonexistent shadow instance", "crm_shadow --switch {shadow} --batch {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.NOSUCH), make_test_group("Switch to nonexistent shadow instance (force)", "crm_shadow --switch {shadow} --batch --force {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.NOSUCH), ], setup="crm_shadow --delete {shadow} --force"), ] return no_instance_tests + [ ShadowTestGroup(new_instance_tests + more_tests, env={"CIB_file": "{cts_cli_data}/crm_mon.xml"}, create=False), ShadowTestGroup(delete_1_tests, env={"CIB_file": "{cts_cli_data}/crm_mon.xml"}), ShadowTestGroup(delete_2_tests, env={"CIB_file": "{cts_cli_data}/crm_mon.xml", "CIB_shadow": None}), ShadowTestGroup(delete_3_tests, env={"CIB_file": "{cts_cli_data}/crm_mon.xml", "CIB_shadow": None}), ShadowTestGroup(delete_4_tests, env={"CIB_file": "{cts_cli_data}/crm_mon.xml", "CIB_shadow": "nonexistent_shadow"}), ShadowTestGroup(delete_5_tests, env={"CIB_file": "{cts_cli_data}/crm_mon.xml", "CIB_shadow": "nonexistent_shadow"}), ShadowTestGroup(delete_6_tests, env={"CIB_file": "{cts_cli_data}/nonexistent_cib.xml"}), ShadowTestGroup(delete_7_tests, env={"CIB_file": "{cts_cli_data}/nonexistent_cib.xml"}), ShadowTestGroup(create_1_tests, env={"CIB_file": "{cts_cli_data}/crm_mon.xml"}, create=False), ShadowTestGroup(create_2_tests, env={"CIB_file": "{cts_cli_data}/crm_mon.xml"}, create=False), ShadowTestGroup(reset_1_tests, env={"CIB_file": "{cts_cli_data}/crm_mon.xml"}, create_empty=True), ShadowTestGroup(reset_2_tests, env={"CIB_file": "{cts_cli_data}/crm_mon.xml", "CIB_shadow": None}, create_empty=True), ShadowTestGroup(reset_3_tests, env={"CIB_file": "{cts_cli_data}/crm_mon.xml", "CIB_shadow": "nonexistent_shadow"}, create_empty=True), ShadowTestGroup(reset_4_tests, env={"CIB_file": "{cts_cli_data}/nonexistent_cib.xml"}, create_empty=True), ShadowTestGroup(switch_tests, env={"CIB_shadow": "nonexistent_shadow"}, create_empty=True), ] class CrmVerifyRegressionTest(RegressionTest): """A class for testing crm_verify.""" @property def name(self): """Return the name of this regression test.""" return "crm_verify" @property def tests(self): """A list of Test instances to be run as part of this regression test.""" invalid_tests = [ Test("Verbosely verify a file-specified configuration with an unallowed fencing level ID", "crm_verify --xml-file {cts_cli_data}/crm_verify_invalid_fencing_topology.xml --verbose", expected_rc=ExitStatus.CONFIG), make_test_group("Verify a file-specified invalid configuration", "crm_verify --xml-file {cts_cli_data}/crm_verify_invalid_bz.xml {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.CONFIG), make_test_group("Verify a file-specified invalid configuration (verbose)", "crm_verify --xml-file {cts_cli_data}/crm_verify_invalid_bz.xml --verbose {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.CONFIG), make_test_group("Verify a file-specified invalid configuration (quiet)", "crm_verify --xml-file {cts_cli_data}/crm_verify_invalid_bz.xml --quiet {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.CONFIG), ValidatingTest("Verify another file-specified invalid configuration", "crm_verify --xml-file {cts_cli_data}/crm_verify_invalid_no_stonith.xml --output-as=xml", expected_rc=ExitStatus.CONFIG), ] with open("%s/cli/crm_mon.xml" % test_home, encoding="utf-8") as f: cib_contents = f.read() valid_tests = [ ValidatingTest("Verify a file-specified valid configuration", "crm_verify --xml-file {cts_cli_data}/crm_mon.xml --output-as=xml"), ValidatingTest("Verify a piped-in valid configuration", "crm_verify -p --output-as=xml", stdin=pathlib.Path(apply_substitutions("{cts_cli_data}/crm_mon.xml"))), ValidatingTest("Verbosely verify a file-specified valid configuration", "crm_verify --xml-file {cts_cli_data}/crm_mon.xml --output-as=xml --verbose"), ValidatingTest("Verbosely verify a piped-in valid configuration", "crm_verify -p --output-as=xml --verbose", stdin=pathlib.Path(apply_substitutions("{cts_cli_data}/crm_mon.xml"))), ValidatingTest("Verify a string-supplied valid configuration", "crm_verify -X '%s' --output-as=xml" % cib_contents), ValidatingTest("Verbosely verify a string-supplied valid configuration", "crm_verify -X '%s' --output-as=xml --verbose" % cib_contents), ] return invalid_tests + valid_tests class CrmMonRegressionTest(RegressionTest): """A class for testing crm_mon.""" @property def name(self): """Return the name of this regression test.""" return "crm_mon" @property def tests(self): """A list of Test instances to be run as part of this regression test.""" basic_tests = [ make_test_group("Basic output", "crm_mon -1 {fmt}", [Test, ValidatingTest]), make_test_group("Output without node section", "crm_mon -1 --exclude=nodes {fmt}", [Test, ValidatingTest]), # The next test doesn't need to be performed for other output formats. It's # really just a test to make sure that blank lines are correct. Test("Output with only the node section", "crm_mon -1 --exclude=all --include=nodes"), # XML includes everything already so there's no need for a complete test Test("Complete text output", "crm_mon -1 --include=all"), # XML includes detailed output already Test("Complete text output with detail", "crm_mon -1R --include=all"), Test("Complete brief text output", "crm_mon -1 --include=all --brief"), Test("Complete text output grouped by node", "crm_mon -1 --include=all --group-by-node"), # XML does not have a brief output option Test("Complete brief text output grouped by node", "crm_mon -1 --include=all --group-by-node --brief"), ValidatingTest("Output grouped by node", "crm_mon --output-as=xml --group-by-node"), make_test_group("Complete output filtered by node", "crm_mon -1 --include=all --node=cluster01 {fmt}", [Test, ValidatingTest]), make_test_group("Complete output filtered by tag", "crm_mon -1 --include=all --node=even-nodes {fmt}", [Test, ValidatingTest]), make_test_group("Complete output filtered by resource tag", "crm_mon -1 --include=all --resource=fencing-rscs {fmt}", [Test, ValidatingTest]), make_test_group("Output filtered by node that doesn't exist", "crm_mon -1 --node=blah {fmt}", [Test, ValidatingTest]), Test("Basic text output with inactive resources", "crm_mon -1 -r"), # XML already includes inactive resources Test("Basic text output with inactive resources, filtered by node", "crm_mon -1 -r --node=cluster02"), make_test_group("Complete output filtered by primitive resource", "crm_mon -1 --include=all --resource=Fencing {fmt}", [Test, ValidatingTest]), make_test_group("Complete output filtered by group resource", "crm_mon -1 --include=all --resource=exim-group {fmt}", [Test, ValidatingTest]), Test("Complete text output filtered by group resource member", "crm_mon -1 --include=all --resource=Public-IP"), ValidatingTest("Output filtered by group resource member", "crm_mon --output-as=xml --resource=Email"), make_test_group("Complete output filtered by clone resource", "crm_mon -1 --include=all --resource=ping-clone {fmt}", [Test, ValidatingTest]), make_test_group("Complete output filtered by clone resource instance", "crm_mon -1 --include=all --resource=ping {fmt}", [Test, ValidatingTest]), Test("Complete text output filtered by exact clone resource instance", "crm_mon -1 --include=all --show-detail --resource=ping:0"), ValidatingTest("Output filtered by exact clone resource instance", "crm_mon --output-as=xml --resource=ping:1"), make_test_group("Output filtered by resource that doesn't exist", "crm_mon -1 --resource=blah {fmt}", [Test, ValidatingTest]), Test("Basic text output with inactive resources, filtered by tag", "crm_mon -1 -r --resource=inactive-rscs"), Test("Basic text output with inactive resources, filtered by bundle resource", "crm_mon -1 -r --resource=httpd-bundle"), ValidatingTest("Output filtered by inactive bundle resource", "crm_mon --output-as=xml --resource=httpd-bundle"), Test("Basic text output with inactive resources, filtered by bundled IP address resource", "crm_mon -1 -r --resource=httpd-bundle-ip-192.168.122.131"), ValidatingTest("Output filtered by bundled IP address resource", "crm_mon --output-as=xml --resource=httpd-bundle-ip-192.168.122.132"), Test("Basic text output with inactive resources, filtered by bundled container", "crm_mon -1 -r --resource=httpd-bundle-docker-1"), ValidatingTest("Output filtered by bundled container", "crm_mon --output-as=xml --resource=httpd-bundle-docker-2"), Test("Basic text output with inactive resources, filtered by bundle connection", "crm_mon -1 -r --resource=httpd-bundle-0"), ValidatingTest("Output filtered by bundle connection", "crm_mon --output-as=xml --resource=httpd-bundle-0"), Test("Basic text output with inactive resources, filtered by bundled primitive resource", "crm_mon -1 -r --resource=httpd"), ValidatingTest("Output filtered by bundled primitive resource", "crm_mon --output-as=xml --resource=httpd"), Test("Complete text output, filtered by clone name in cloned group", "crm_mon -1 --include=all --show-detail --resource=mysql-clone-group"), ValidatingTest("Output, filtered by clone name in cloned group", "crm_mon --output-as=xml --resource=mysql-clone-group"), Test("Complete text output, filtered by group name in cloned group", "crm_mon -1 --include=all --show-detail --resource=mysql-group"), ValidatingTest("Output, filtered by group name in cloned group", "crm_mon --output-as=xml --resource=mysql-group"), Test("Complete text output, filtered by exact group instance name in cloned group", "crm_mon -1 --include=all --show-detail --resource=mysql-group:1"), ValidatingTest("Output, filtered by exact group instance name in cloned group", "crm_mon --output-as=xml --resource=mysql-group:1"), Test("Complete text output, filtered by primitive name in cloned group", "crm_mon -1 --include=all --show-detail --resource=mysql-proxy"), ValidatingTest("Output, filtered by primitive name in cloned group", "crm_mon --output-as=xml --resource=mysql-proxy"), Test("Complete text output, filtered by exact primitive instance name in cloned group", "crm_mon -1 --include=all --show-detail --resource=mysql-proxy:1"), ValidatingTest("Output, filtered by exact primitive instance name in cloned group", "crm_mon --output-as=xml --resource=mysql-proxy:1"), ] partial_tests = [ Test("Output of partially active resources", "crm_mon -1 --show-detail"), ValidatingTest("Output of partially active resources", "crm_mon --output-as=xml"), Test("Output of partially active resources, with inactive resources", "crm_mon -1 -r --show-detail"), # XML already includes inactive resources Test("Complete brief text output, with inactive resources", "crm_mon -1 -r --include=all --brief --show-detail"), # XML does not have a brief output option Test("Text output of partially active group", "crm_mon -1 --resource=partially-active-group"), Test("Text output of partially active group, with inactive resources", "crm_mon -1 --resource=partially-active-group -r"), Test("Text output of active member of partially active group", "crm_mon -1 --resource=dummy-1"), Test("Text output of inactive member of partially active group", "crm_mon -1 --resource=dummy-2 --show-detail"), Test("Complete brief text output grouped by node, with inactive resources", "crm_mon -1 -r --include=all --group-by-node --brief --show-detail"), Test("Text output of partially active resources, with inactive resources, filtered by node", "crm_mon -1 -r --node=cluster01"), ValidatingTest("Output of partially active resources, filtered by node", "crm_mon --output-as=xml --node=cluster01"), ] unmanaged_tests = [ make_test_group("Output of active unmanaged resource on offline node", "crm_mon -1 {fmt}", [Test, ValidatingTest]), Test("Brief text output of active unmanaged resource on offline node", "crm_mon -1 --brief"), Test("Brief text output of active unmanaged resource on offline node, grouped by node", "crm_mon -1 --brief --group-by-node"), ] maint1_tests = [ make_test_group("Output of all resources with maintenance-mode enabled", "crm_mon -1 -r {fmt}", [Test, ValidatingTest], setup="crm_attribute -n maintenance-mode -v true", teardown="crm_attribute -n maintenance-mode -v false"), make_test_group("Output of all resources with maintenance enabled for a node", "crm_mon -1 -r {fmt}", [Test, ValidatingTest], setup="crm_attribute -n maintenance -N cluster02 -v true", teardown="crm_attribute -n maintenance -N cluster02 -v false"), ] maint2_tests = [ # The fence resource is excluded, for comparison make_test_group("Output of all resources with maintenance meta attribute true", "crm_mon -1 -r {fmt}", [Test, ValidatingTest]), ] t180_tests = [ Test("Text output of guest node's container on different node from its remote resource", "crm_mon -1"), Test("Complete text output of guest node's container on different node from its remote resource", "crm_mon -1 --show-detail"), ] return [ TestGroup(basic_tests, env={"CIB_file": "{cts_cli_data}/crm_mon.xml"}), Test("Check that CIB_file=\"-\" works", "crm_mon -1", env={"CIB_file": "-"}, stdin=pathlib.Path(apply_substitutions("{cts_cli_data}/crm_mon.xml"))), TestGroup(partial_tests, env={"CIB_file": "{cts_cli_data}/crm_mon-partial.xml"}), TestGroup(unmanaged_tests, env={"CIB_file": "{cts_cli_data}/crm_mon-unmanaged.xml"}), TestGroup(maint1_tests, cib_gen=partial(copy_existing_cib, "{cts_cli_data}/crm_mon.xml")), TestGroup(maint2_tests, env={"CIB_file": "{cts_cli_data}/crm_mon-rsc-maint.xml"}), TestGroup(t180_tests, env={"CIB_file": "{cts_cli_data}/crm_mon-T180.xml"}), ] class AclsRegressionTest(RegressionTest): """A class for testing access control lists.""" @property def name(self): """Return the name of this regression test.""" return "acls" @property def tests(self): """A list of Test instances to be run as part of this regression test.""" acl_cib = """ """ basic_tests = [ Test("Configure some ACLs", "cibadmin -M -o acls -p", update_cib=True, stdin=acl_cib), Test("Enable ACLs", "crm_attribute -n enable-acl -v true", update_cib=True), Test("Set cluster option", "crm_attribute -n no-quorum-policy -v ignore", update_cib=True), Test("New ACL role", """cibadmin --create -o acls --xml-text ''""", update_cib=True), Test("New ACL target", """cibadmin --create -o acls --xml-text ''""", update_cib=True), Test("Another ACL role", """cibadmin --create -o acls --xml-text ''""", update_cib=True), Test("Another ACL target", """cibadmin --create -o acls --xml-text ''""", update_cib=True), Test("Updated ACL", """cibadmin --replace -o acls --xml-text ''""", update_cib=True), ] no_acl_tests = [ Test("unknownguy: Query configuration", "cibadmin -Q", expected_rc=ExitStatus.INSUFFICIENT_PRIV), Test("unknownguy: Set enable-acl", "crm_attribute -n enable-acl -v false", expected_rc=ExitStatus.INSUFFICIENT_PRIV), Test("unknownguy: Set stonith-enabled", "crm_attribute -n stonith-enabled -v false", expected_rc=ExitStatus.INSUFFICIENT_PRIV), Test("unknownguy: Create a resource", """cibadmin -C -o resources --xml-text ''""", expected_rc=ExitStatus.INSUFFICIENT_PRIV), ] deny_cib_tests = [ Test("l33t-haxor: Query configuration", "cibadmin -Q", expected_rc=ExitStatus.INSUFFICIENT_PRIV), Test("l33t-haxor: Set enable-acl", "crm_attribute -n enable-acl -v false", expected_rc=ExitStatus.INSUFFICIENT_PRIV), Test("l33t-haxor: Set stonith-enabled", "crm_attribute -n stonith-enabled -v false", expected_rc=ExitStatus.INSUFFICIENT_PRIV), Test("l33t-haxor: Create a resource", """cibadmin -C -o resources --xml-text ''""", expected_rc=ExitStatus.INSUFFICIENT_PRIV), ] observer_tests = [ Test("niceguy: Query configuration", "cibadmin -Q"), Test("niceguy: Set enable-acl", "crm_attribute -n enable-acl -v false", expected_rc=ExitStatus.INSUFFICIENT_PRIV), Test("niceguy: Set stonith-enabled", "crm_attribute -n stonith-enabled -v false", update_cib=True), Test("niceguy: Create a resource", """cibadmin -C -o resources --xml-text ''""", expected_rc=ExitStatus.INSUFFICIENT_PRIV), Test("root: Query configuration", "cibadmin -Q", env={"CIB_user": "root"}), Test("root: Set stonith-enabled", "crm_attribute -n stonith-enabled -v true", update_cib=True, env={"CIB_user": "root"}), Test("root: Create a resource", """cibadmin -C -o resources --xml-text ''""", update_cib=True, env={"CIB_user": "root"}), ] deny_cib_2_tests = [ Test("l33t-haxor: Create a resource meta attribute", "crm_resource -r dummy --meta -p target-role -v Stopped", expected_rc=ExitStatus.INSUFFICIENT_PRIV), Test("l33t-haxor: Query a resource meta attribute", "crm_resource -r dummy --meta -g target-role", expected_rc=ExitStatus.INSUFFICIENT_PRIV), Test("l33t-haxor: Remove a resource meta attribute", "crm_resource -r dummy --meta -d target-role", expected_rc=ExitStatus.INSUFFICIENT_PRIV), ] observer_2_tests = [ Test("niceguy: Create a resource meta attribute", "crm_resource -r dummy --meta -p target-role -v Stopped", update_cib=True), Test("niceguy: Query a resource meta attribute", "crm_resource -r dummy --meta -g target-role", update_cib=True), Test("niceguy: Remove a resource meta attribute", "crm_resource -r dummy --meta -d target-role", update_cib=True), Test("niceguy: Create a resource meta attribute", "crm_resource -r dummy --meta -p target-role -v Started", update_cib=True), ] read_meta_tests = [ Test("badidea: Query configuration - implied deny", "cibadmin -Q"), ] deny_cib_3_tests = [ Test("betteridea: Query configuration - explicit deny", "cibadmin -Q"), ] replace_tests = [ TestGroup([ AclTest("niceguy: Replace - remove acls", "cibadmin --replace -p", setup="cibadmin --delete --xml-text ''", expected_rc=ExitStatus.INSUFFICIENT_PRIV), AclTest("niceguy: Replace - create resource", "cibadmin --replace -p", setup="""cibadmin -C -o resources --xml-text ''""", expected_rc=ExitStatus.INSUFFICIENT_PRIV), AclTest("niceguy: Replace - modify attribute (deny)", "cibadmin --replace -p", setup="crm_attribute -n enable-acl -v false", expected_rc=ExitStatus.INSUFFICIENT_PRIV), AclTest("niceguy: Replace - delete attribute (deny)", "cibadmin --replace -p", setup="""cibadmin --replace --xml-text ''""", expected_rc=ExitStatus.INSUFFICIENT_PRIV), AclTest("niceguy: Replace - create attribute (deny)", "cibadmin --replace -p", setup="""cibadmin --modify --xml-text ''""", expected_rc=ExitStatus.INSUFFICIENT_PRIV), ], env={"CIB_user": "niceguy"}), # admin role TestGroup([ AclTest("bob: Replace - create attribute (direct allow)", "cibadmin --replace -o resources -p", setup="""cibadmin --modify --xml-text ''"""), AclTest("bob: Replace - modify attribute (direct allow)", "cibadmin --replace -o resources -p", setup="""cibadmin --modify --xml-text ''"""), AclTest("bob: Replace - delete attribute (direct allow)", "cibadmin --replace -o resources -p", setup="""cibadmin --replace -o resources --xml-text ''"""), ], env={"CIB_user": "bob"}), # super_user role TestGroup([ AclTest("joe: Replace - create attribute (inherited allow)", "cibadmin --replace -o resources -p", setup="""cibadmin --modify --xml-text ''"""), AclTest("joe: Replace - modify attribute (inherited allow)", "cibadmin --replace -o resources -p", setup="""cibadmin --modify --xml-text ''"""), AclTest("joe: Replace - delete attribute (inherited allow)", "cibadmin --replace -o resources -p", setup="""cibadmin --replace -o resources --xml-text ''"""), ], env={"CIB_user": "joe"}), # rsc_writer role TestGroup([ AclTest("mike: Replace - create attribute (allow overrides deny)", "cibadmin --replace -o resources -p", setup="""cibadmin --modify --xml-text ''"""), AclTest("mike: Replace - modify attribute (allow overrides deny)", "cibadmin --replace -o resources -p", setup="""cibadmin --modify --xml-text ''"""), AclTest("mike: Replace - delete attribute (allow overrides deny)", "cibadmin --replace -o resources -p", setup="""cibadmin --replace -o resources --xml-text ''"""), # Create an additional resource for deny-overrides-allow testing AclTest("mike: Create another resource", """cibadmin -C -o resources --xml-text ''""", update_cib=True), ], env={"CIB_user": "mike"}), # rsc_denied role TestGroup([ AclTest("chris: Replace - create attribute (deny overrides allow)", "cibadmin --replace -o resources -p", setup="""cibadmin --modify --xml-text ''""", expected_rc=ExitStatus.INSUFFICIENT_PRIV), AclTest("chris: Replace - modify attribute (deny overrides allow)", "cibadmin --replace -o resources -p", setup="""cibadmin --modify --xml-text ''""", expected_rc=ExitStatus.INSUFFICIENT_PRIV), AclTest("chris: Replace - delete attribute (deny overrides allow)", "cibadmin --replace -o resources -p", setup="""cibadmin --replace -o resources --xml-text ''""", expected_rc=ExitStatus.INSUFFICIENT_PRIV), ], env={"CIB_user": "chris"}), ] loop_tests = [ # no ACL TestGroup(no_acl_tests, env={"CIB_user": "unknownguy"}), # deny /cib permission TestGroup(deny_cib_tests, env={"CIB_user": "l33t-haxor"}), # observer role TestGroup(observer_tests, env={"CIB_user": "niceguy"}), # deny /cib permission TestGroup(deny_cib_2_tests, env={"CIB_user": "l33t-haxor"}), # observer role TestGroup(observer_2_tests, env={"CIB_user": "niceguy"}), # read //meta_attributes TestGroup(read_meta_tests, env={"CIB_user": "badidea"}), # deny /cib, read //meta_attributes TestGroup(deny_cib_3_tests, env={"CIB_user": "betteridea"}), ] + replace_tests return [ ShadowTestGroup(basic_tests + [ TestGroup(loop_tests, env={"PCMK_trace_functions": "pcmk__check_acl,pcmk__apply_creation_acl"})]), ] class ValidityRegressionTest(RegressionTest): """A class for testing CIB validity.""" @property def name(self): """Return the name of this regression test.""" return "validity" @property def tests(self): """A list of Test instances to be run as part of this regression test.""" basic_tests = [ Test("Try to make resulting CIB invalid (enum violation)", """cibadmin -M -o constraints --xml-text ''""", expected_rc=ExitStatus.CONFIG, update_cib=True), Test("Run crm_simulate with invalid CIB (enum violation)", "crm_simulate -p -S", stdin=StdinCmd("""cibadmin -Q | sed 's#"start"#"break"#'"""), expected_rc=ExitStatus.CONFIG), Test("Try to make resulting CIB invalid (unrecognized validate-with)", "cibadmin -M --xml-text ''", expected_rc=ExitStatus.CONFIG, update_cib=True), Test("Run crm_simulate with invalid CIB (unrecognized validate-with)", "crm_simulate -p -S", stdin=StdinCmd("""cibadmin -Q | sed 's#"pacemaker-1.2"#"pacemaker-9999.0"#'"""), expected_rc=ExitStatus.CONFIG), Test("Try to make resulting CIB invalid, but possibly recoverable (valid with X.Y+1)", "cibadmin -C -o configuration --xml-text ''", expected_rc=ExitStatus.CONFIG, update_cib=True), Test("Run crm_simulate with invalid, but possibly recoverable CIB (valid with X.Y+1)", "crm_simulate -p -S", stdin=StdinCmd("cibadmin -Q | sed 's###'")), Test("Make resulting CIB valid, although without validate-with attribute", "cibadmin -p -R", stdin=StdinCmd("""cibadmin -Q | sed 's#[ ][ ]*validate-with="[^"]*"##'"""), update_cib=True), Test("Run crm_simulate with valid CIB, but without validate-with attribute", "crm_simulate -p -S", stdin=StdinCmd("cibadmin -Q")), # this will just disable validation and accept the config, outputting # validation errors Test("Make resulting CIB invalid, and without validate-with attribute", "cibadmin -p -R", stdin=StdinCmd("""cibadmin -Q | """ """sed -e 's#[ ][ ]*validate-with="[^"]*"##' """ """ -e 's#\\([ ][ ]*epoch="[^"]*\\)"#\\10"#' """ """ -e 's#"start"#"break"#'"""), update_cib=True), Test("Run crm_simulate with invalid CIB, also without validate-with attribute", "crm_simulate -p -S", stdin=StdinCmd("""cibadmin -Q | """ """sed -e 's#[ ][ ]*validate-with="[^"]*"##' """ """ -e 's#\\([ ][ ]*epoch="[^"]*\\)"#\\10"#' """ """ -e 's#"start"#"break"#'""")), ] basic_tests_setup = [ """cibadmin -C -o resources --xml-text ''""", """cibadmin -C -o resources --xml-text ''""", """cibadmin -C -o constraints --xml-text ''""", ] return [ ShadowTestGroup(basic_tests, validate_with="pacemaker-1.2", setup=basic_tests_setup, env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"}), ] class UpgradeRegressionTest(RegressionTest): """A class for testing upgrading the CIB.""" @property def name(self): """Return the name of this regression test.""" return "upgrade" @property def tests(self): """A list of Test instances to be run as part of this regression test.""" resource_cib = """ """ basic_tests = [ Test("Set stonith-enabled=false", "crm_attribute -n stonith-enabled -v false", update_cib=True), Test("Configure the initial resource", "cibadmin -M -o resources -p", update_cib=True, stdin=resource_cib), Test("Upgrade to latest CIB schema (trigger 2.10.xsl + the wrapping)", "cibadmin --upgrade --force -V -V", update_cib=True), Test("Query a resource instance attribute (shall survive)", "crm_resource -r mySmartFuse -g requires", update_cib=True), ] return [ ShadowTestGroup(basic_tests, validate_with="pacemaker-2.10", env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"}) ] class RulesRegressionTest(RegressionTest): """A class for testing support for CIB rules.""" @property def name(self): """Return the name of this regression test.""" return "rules" @property def tests(self): """A list of Test instances to be run as part of this regression test.""" tomorrow = datetime.now() + timedelta(days=1) rule_cib = """ """ % tomorrow.strftime("%F %T %z") usage_tests = [ make_test_group("crm_rule given no arguments", "crm_rule {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.USAGE), make_test_group("crm_rule given no rule to check", "crm_rule -c {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.USAGE), make_test_group("crm_rule given invalid input XML", "crm_rule -c -r blahblah -X invalidxml {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.DATAERR), make_test_group("crm_rule given invalid input XML on stdin", "crm_rule -c -r blahblah -X - {fmt}", [Test, ValidatingTest], stdin=StdinCmd("echo invalidxml"), expected_rc=ExitStatus.DATAERR), ] basic_tests = [ make_test_group("Try to check a rule that doesn't exist", "crm_rule -c -r blahblah {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.NOSUCH), make_test_group("Try to check a rule that has too many date_expressions", "crm_rule -c -r cli-rule-too-many-date-expressions {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.UNIMPLEMENT_FEATURE), make_test_group("Verify basic rule is expired", "crm_rule -c -r cli-prefer-rule-dummy-expired {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.EXPIRED), make_test_group("Verify basic rule worked in the past", "crm_rule -c -r cli-prefer-rule-dummy-expired -d 20180101 {fmt}", [Test, ValidatingTest]), make_test_group("Verify basic rule is not yet in effect", "crm_rule -c -r cli-prefer-rule-dummy-not-yet {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.NOT_YET_IN_EFFECT), make_test_group("Verify date_spec rule with years has expired", "crm_rule -c -r cli-prefer-rule-dummy-date_spec-only-years {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.EXPIRED), make_test_group("Verify multiple rules at once", "crm_rule -c -r cli-prefer-rule-dummy-not-yet -r cli-prefer-rule-dummy-date_spec-only-years {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.EXPIRED), make_test_group("Verify date_spec rule with years is in effect", "crm_rule -c -r cli-prefer-rule-dummy-date_spec-only-years -d 20190201 {fmt}", [Test, ValidatingTest]), make_test_group("Try to check a rule whose date_spec does not contain years=", "crm_rule -c -r cli-prefer-rule-dummy-date_spec-without-years {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.UNIMPLEMENT_FEATURE), make_test_group("Try to check a rule whose date_spec contains years= and moon=", "crm_rule -c -r cli-prefer-rule-dummy-date_spec-years-moon {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.UNIMPLEMENT_FEATURE), make_test_group("Try to check a rule with no date_expression", "crm_rule -c -r cli-no-date_expression-rule {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.UNIMPLEMENT_FEATURE), ] return usage_tests + [ TestGroup(basic_tests, cib_gen=partial(write_cib, rule_cib)) ] class FeatureSetRegressionTest(RegressionTest): """A class for testing support for version-specific features.""" @property def name(self): """Return the name of this regression test.""" return "feature_set" @property def tests(self): """A list of Test instances to be run as part of this regression test.""" basic_tests = [ # Import the test CIB Test("Import the test CIB", "cibadmin --replace --xml-file {cts_cli_data}/crm_mon-feature_set.xml", update_cib=True), Test("Complete text output, no mixed status", "crm_mon -1 --show-detail"), ValidatingTest("Output, no mixed status", "crm_mon --output-as=xml"), # Modify the CIB to fake that the cluster has mixed versions Test("Fake inconsistent feature set", "crm_attribute --node=cluster02 --name=#feature-set --update=3.15.0 --lifetime=reboot", update_cib=True), Test("Complete text output, mixed status", "crm_mon -1 --show-detail"), ValidatingTest("Output, mixed status", "crm_mon --output-as=xml"), ] return [ ShadowTestGroup(basic_tests), ] # Tests that depend on resource agents and must be run in an installed # environment class AgentRegressionTest(RegressionTest): """A class for testing resource agents.""" @property def name(self): """Return the name of this regression test.""" return "agents" @property def tests(self): """A list of Test instances to be run as part of this regression test.""" return [ make_test_group("Validate a valid resource configuration", "crm_resource --validate --class ocf --provider pacemaker --agent Dummy {fmt}", [Test, ValidatingTest]), # Make the Dummy configuration invalid (op_sleep can't be a generic string) make_test_group("Validate an invalid resource configuration", "crm_resource --validate --class ocf --provider pacemaker --agent Dummy {fmt}", [Test, ValidatingTest], expected_rc=ExitStatus.NOT_CONFIGURED, env={"OCF_RESKEY_op_sleep": "asdf"}), ] def build_options(): """Handle command line arguments.""" parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description="Command line tool regression tests", epilog="Default tests: %s\n" "Other tests: agents (must be run in an installed environment)" % " ".join(default_tests)) parser.add_argument("-j", "--jobs", metavar="JOBS", default=cpu_count() - 1, type=int, help="The number of tests to run simultaneously") parser.add_argument("-p", "--path", metavar="DIR", action="append", help="Look for executables in DIR (may be specified multiple times)") parser.add_argument("-r", "--run-only", metavar="TEST", choices=default_tests + ["tools"] + other_tests, action="append", help="Run only specified tests (may be specified multiple times)") parser.add_argument("-s", "--save", action="store_true", help="Save actual output as expected output") parser.add_argument("-v", "--valgrind", action="store_true", help="Run all commands under valgrind") parser.add_argument("-V", "--verbose", action="store_true", help="Display any differences from expected output") args = parser.parse_args() if args.path is None: args.path = [] return args def setup_environment(valgrind): """Set various environment variables needed for operation.""" if valgrind: os.environ["G_SLICE"] = "always-malloc" # Ensure all command output is in portable locale for comparison os.environ["LC_ALL"] = "C" # Log test errors to stderr os.environ["PCMK_stderr"] = "1" # Because we will change the value of PCMK_trace_functions and then reset it # back to some initial value at various points, it's easiest to assume it is # defined but empty by default if "PCMK_trace_functions" not in os.environ: os.environ["PCMK_trace_functions"] = "" def path_prepend(p): """Add another directory to the front of $PATH.""" old = os.environ["PATH"] os.environ["PATH"] = "%s:%s" % (p, old) def setup_path(opts_path): """Set the PATH environment variable appropriately for the tests.""" srcdir = os.path.dirname(test_home) # Add any search paths given on the command line for p in opts_path: path_prepend(p) if os.path.exists("%s/tools/crm_simulate" % srcdir): print("Using local binaries from: %s" % srcdir) path_prepend("%s/tools" % srcdir) for daemon in ["based", "controld", "fenced", "schedulerd"]: path_prepend("%s/daemons/%s" % (srcdir, daemon)) print("Using local schemas from: %s/xml" % srcdir) os.environ["PCMK_schema_directory"] = "%s/xml" % srcdir else: path_prepend(BuildOptions.DAEMON_DIR) os.environ["PCMK_schema_directory"] = BuildOptions.SCHEMA_DIR def _run_one(valgrind, r): """Run and return a TestGroup object.""" # See comments in run_regression_tests. r.run(valgrind=valgrind) return r def run_regression_tests(regs, jobs, valgrind=False): """Run the given tests and return the modified objects.""" executed = [] with Pool(processes=jobs) as pool: # What we really want to do here is: # pool.map(lambda r: r.run(),regs) # # However, multiprocessing uses pickle somehow in its operation, and python # doesn't want to pickle a lambda (nor a nested function within this one). # Thus, we need to use the _run_one wrapper at the file level just to call # run(). Further, if we don't return the modified object from that and then # return the list of modified objects here, it looks like the rest of the # program will use the originals, before this was ever run. executed = pool.map(partial(_run_one, valgrind), regs) return executed def results(regs, save, verbose): """Print the output from each regression test, returning the number whose output differs.""" output_differs = 0 if verbose: print("\n\nResults") for r in regs: r.write() if save: dest = "%s/cli/regression.%s.exp" % (test_home, r.name) copyfile(r.results_file, dest) r.diff() if not r.identical: output_differs += 1 return output_differs def summary(regs, output_differs, verbose): """Print the summary output for the entire test run.""" test_failures = 0 test_successes = 0 for r in regs: test_failures += r.failures test_successes += r.successes print("\n\nSummary") # First, print all the Passed/Failed lines from each Test run. for r in regs: print("\n".join(r.summary)) fmt = PluralFormatter() # Then, print information specific to each result possibility. Basically, # if there were failures then we print the output differences, leave the # failed output files in place, and exit with an error. Otherwise, clean up # anything that passed. if test_failures > 0 and output_differs > 0: print(fmt.format("{0} {0:plural,test} failed; see output in:", test_failures)) for r in regs: r.process_results(verbose) return ExitStatus.ERROR if test_failures > 0: print(fmt.format("{0} {0:plural,test} failed", test_failures)) for r in regs: r.process_results(verbose) return ExitStatus.ERROR if output_differs: print(fmt.format("{0} {0:plural,test} passed but output was " "unexpected; see output in:", test_successes)) for r in regs: r.process_results(verbose) return ExitStatus.DIGEST print(fmt.format("{0} {0:plural,test} passed", test_successes)) for r in regs: r.cleanup() return ExitStatus.OK regression_classes = [ AccessRenderRegressionTest, DaemonsRegressionTest, DatesRegressionTest, ErrorCodeRegressionTest, CibadminRegressionTest, CrmAttributeRegressionTest, CrmStandbyRegressionTest, CrmResourceRegressionTest, CrmTicketRegressionTest, CrmadminRegressionTest, CrmShadowRegressionTest, CrmVerifyRegressionTest, CrmMonRegressionTest, AclsRegressionTest, ValidityRegressionTest, UpgradeRegressionTest, RulesRegressionTest, FeatureSetRegressionTest, AgentRegressionTest, ] def main(): """Run command line regression tests as specified by arguments.""" opts = build_options() setup_environment(opts.valgrind) setup_path(opts.path) # Filter the list of all regression test classes to include only those that # were requested on the command line. If empty, this defaults to default_tests. if not opts.run_only: opts.run_only = default_tests if opts.run_only == ["tools"]: opts.run_only = tools_tests regs = [] for cls in regression_classes: obj = cls() if obj.name in opts.run_only: regs.append(obj) regs = run_regression_tests(regs, max(1, opts.jobs), valgrind=opts.valgrind) output_differs = results(regs, opts.save, opts.verbose) rc = summary(regs, output_differs, opts.verbose) sys.exit(rc) if __name__ == "__main__": main() # vim: set filetype=python expandtab tabstop=4 softtabstop=4 shiftwidth=4 textwidth=120: diff --git a/doc/sphinx/Pacemaker_Development/c.rst b/doc/sphinx/Pacemaker_Development/c.rst index 5c0425c2d6..e519324c40 100644 --- a/doc/sphinx/Pacemaker_Development/c.rst +++ b/doc/sphinx/Pacemaker_Development/c.rst @@ -1,1074 +1,1128 @@ .. index:: single: C pair: C; guidelines C Coding Guidelines ------------------- Pacemaker is a large project accepting contributions from developers with a wide range of skill levels and organizational affiliations, and maintained by multiple people over long periods of time. Following consistent guidelines makes reading, writing, and reviewing code easier, and helps avoid common mistakes. Some existing Pacemaker code does not follow these guidelines, for historical reasons and API backward compatibility, but new code should. Code Organization ################# Pacemaker's C code is organized as follows: +-----------------+-----------------------------------------------------------+ | Directory | Contents | +=================+===========================================================+ | daemons | the Pacemaker daemons (pacemakerd, pacemaker-based, etc.) | +-----------------+-----------------------------------------------------------+ | include | header files for library APIs | +-----------------+-----------------------------------------------------------+ | lib | libraries | +-----------------+-----------------------------------------------------------+ | tools | command-line tools | +-----------------+-----------------------------------------------------------+ Source file names should be unique across the entire project, to allow for individual tracing via ``PCMK_trace_files``. .. index:: single: C; library single: C library Pacemaker Libraries ################### +---------------+---------+---------------+---------------------------+-------------------------------------+ | Library | Symbol | Source | API Headers | Description | | | prefix | location | | | +===============+=========+===============+===========================+=====================================+ | libcib | cib | lib/cib | | include/crm/cib.h | .. index:: | | | | | | include/crm/cib/* | single: C library; libcib | | | | | | single: libcib | | | | | | | | | | | | API for pacemaker-based IPC and | | | | | | the CIB | +---------------+---------+---------------+---------------------------+-------------------------------------+ | libcrmcluster | pcmk | lib/cluster | | include/crm/cluster.h | .. index:: | | | | | | include/crm/cluster/* | single: C library; libcrmcluster | | | | | | single: libcrmcluster | | | | | | | | | | | | Abstract interface to underlying | | | | | | cluster layer | +---------------+---------+---------------+---------------------------+-------------------------------------+ | libcrmcommon | pcmk | lib/common | | include/crm/common/* | .. index:: | | | | | | some of include/crm/* | single: C library; libcrmcommon | | | | | | single: libcrmcommon | | | | | | | | | | | | Everything else | +---------------+---------+---------------+---------------------------+-------------------------------------+ | libcrmservice | svc | lib/services | | include/crm/services.h | .. index:: | | | | | | single: C library; libcrmservice | | | | | | single: libcrmservice | | | | | | | | | | | | Abstract interface to supported | | | | | | resource types (OCF, LSB, etc.) | +---------------+---------+---------------+---------------------------+-------------------------------------+ | liblrmd | lrmd | lib/lrmd | | include/crm/lrmd*.h | .. index:: | | | | | | single: C library; liblrmd | | | | | | single: liblrmd | | | | | | | | | | | | API for pacemaker-execd IPC | +---------------+---------+---------------+---------------------------+-------------------------------------+ | libpacemaker | pcmk | lib/pacemaker | | include/pacemaker*.h | .. index:: | | | | | | include/pcmki/* | single: C library; libpacemaker | | | | | | single: libpacemaker | | | | | | | | | | | | High-level APIs equivalent to | | | | | | command-line tool capabilities | | | | | | (and high-level internal APIs) | +---------------+---------+---------------+---------------------------+-------------------------------------+ | libpe_rules | pe | lib/pengine | | include/crm/pengine/* | .. index:: | | | | | | single: C library; libpe_rules | | | | | | single: libpe_rules | | | | | | | | | | | | Scheduler functionality related | | | | | | to evaluating rules | +---------------+---------+---------------+---------------------------+-------------------------------------+ | libpe_status | pe | lib/pengine | | include/crm/pengine/* | .. index:: | | | | | | single: C library; libpe_status | | | | | | single: libpe_status | | | | | | | | | | | | Low-level scheduler functionality | +---------------+---------+---------------+---------------------------+-------------------------------------+ | libstonithd | stonith | lib/fencing | | include/crm/stonith-ng.h| .. index:: | | | | | | include/crm/fencing/* | single: C library; libstonithd | | | | | | single: libstonithd | | | | | | | | | | | | API for pacemaker-fenced IPC | +---------------+---------+---------------+---------------------------+-------------------------------------+ Public versus Internal APIs ___________________________ Pacemaker libraries have both internal and public APIs. Internal APIs are those used only within Pacemaker; public APIs are those offered (via header files and documentation) for external code to use. Generic functionality needed by Pacemaker itself, such as string processing or XML processing, should remain internal, while functions providing useful high-level access to Pacemaker capabilities should be public. When in doubt, keep APIs internal, because it's easier to expose a previously internal API than hide a previously public API. Internal APIs can be changed as needed. The public API/ABI should maintain a degree of stability so that external applications using it do not need to be rewritten or rebuilt frequently. Many OSes/distributions avoid breaking API/ABI compatibility within a major release, so if Pacemaker breaks compatibility, that significantly delays when OSes can package the new version. Therefore, changes to public APIs should be backward-compatible (as detailed throughout this chapter), unless we are doing a (rare) release where we specifically intend to break compatibility. External applications known to use Pacemaker's public C API include `sbd `_ and dlm_controld. .. index:: pair: C; naming API Symbol Naming _________________ Exposed API symbols (non-``static`` function names, ``struct`` and ``typedef`` names in header files, etc.) must begin with the prefix appropriate to the library (shown in the table at the beginning of this section). This reduces the chance of naming collisions when external software links against the library. The prefix is usually lowercase but may be all-caps for some defined constants and macros. Public API symbols should follow the library prefix with a single underbar (for example, ``pcmk_something``), and internal API symbols with a double underbar (for example, ``pcmk__other_thing``). File-local symbols (such as static functions) and non-library code do not require a prefix, though a unique prefix indicating an executable (controld, crm_mon, etc.) can be helpful when symbols are shared between multiple source files for the executable. API Header File Naming ______________________ * Internal API headers should be named ending in ``_internal.h``, in the same location as public headers, with the exception of libpacemaker, which for historical reasons keeps internal headers in ``include/pcmki/pcmki_*.h``). * If a library needs to share symbols just within the library, header files for these should be named ending in ``_private.h`` and located in the library source directory (not ``include``). Such functions should be declared as ``G_GNUC_INTERNAL``, to aid compiler efficiency (glib defines this symbol appropriately for the compiler). Header files that are not library API are kept in the same directory as the source code they're included from. The easiest way to tell what kind of API a symbol is, is to see where it's declared. If it's in a public header, it's public API; if it's in an internal header, it's internal API; if it's in a library-private header, it's library-private API; otherwise, it's not an API. .. index:: pair: C; API documentation single: Doxygen API Documentation _________________ Pacemaker uses `Doxygen `_ to automatically generate its `online API documentation `_, so all public API (header files, functions, structs, enums, etc.) should be documented with Doxygen comment blocks. Other code may be documented in the same way if desired, with an ``\internal`` tag in the Doxygen comment. Simple example of an internal function with a Doxygen comment block: .. code-block:: c /*! * \internal * \brief Return string length plus 1 * * Return the number of characters in a given string, plus one. * * \param[in] s A string (must not be NULL) * * \return The length of \p s plus 1. */ static int f(const char *s) { return strlen(s) + 1; } Function arguments are marked as ``[in]`` for input only, ``[out]`` for output only, or ``[in,out]`` for both input and output. ``[in,out]`` should be used for struct pointer arguments if the function can change any data accessed via the pointer. For example, if the struct contains a ``GHashTable *`` member, the argument should be marked as ``[in,out]`` if the function inserts data into the table, even if the struct members themselves are not changed. However, an argument is not ``[in,out]`` if something reachable via the argument is modified via a separate argument. For example, both ``pcmk_resource_t`` and ``pcmk_node_t`` contain pointers to their ``pcmk_scheduler_t`` and thus indirectly to each other, but if the function modifies the resource via the resource argument, the node argument does not have to be ``[in,out]``. Public API Deprecation ______________________ Public APIs may not be removed in most Pacemaker releases, but they may be deprecated. When a public API is deprecated, it is moved to a header whose name ends in ``compat.h``. The original header includes the compatibility header only if the ``PCMK_ALLOW_DEPRECATED`` symbol is undefined or defined to 1. This allows external code to continue using the deprecated APIs, but internal code is prevented from using them because the ``crm_internal.h`` header defines the symbol to 0. .. index:: pair: C; boilerplate pair: license; C pair: copyright; C C Boilerplate ############# Every C file should start with a short copyright and license notice: .. code-block:: c /* * Copyright the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under WITHOUT ANY WARRANTY. */ ** should follow the policy set forth in the `COPYING `_ file, generally one of "GNU General Public License version 2 or later (GPLv2+)" or "GNU Lesser General Public License version 2.1 or later (LGPLv2.1+)". Header files should additionally protect against multiple inclusion by defining a unique symbol of the form ``PCMK____H``, and declare C compatibility for inclusion by C++. For example: .. code-block:: c #ifndef PCMK__MY_HEADER__H #define PCMK__MY_HEADER__H // put #include directives here #ifdef __cplusplus extern "C" { #endif // put header code here #ifdef __cplusplus } #endif #endif // PCMK__MY_HEADER__H Public API header files should give a Doxygen file description at the top of the header code. For example: .. code-block:: c /*! * \file * \brief My brief description here * \ingroup core */ .. index:: pair: C; whitespace Line Formatting ############### * Indentation must be 4 spaces, no tabs. * Do not leave trailing whitespace. * Lines should be no longer than 80 characters unless limiting line length hurts readability. .. index:: pair: C; comment Comments ######## .. code-block:: c /* Single-line comments may look like this */ // ... or this /* Multi-line comments should start immediately after the comment opening. * Subsequent lines should start with an aligned asterisk. The comment * closing should be aligned and on a line by itself. */ .. index:: pair: C; operator Operators ######### .. code-block:: c // Operators have spaces on both sides x = a; /* (1) Do not rely on operator precedence; use parentheses when mixing * operators with different priority, for readability. * (2) No space is used after an opening parenthesis or before a closing * parenthesis. */ x = a + b - (c * d); .. index:: single: C; if single: C; else single: C; while single: C; for single: C; switch Control Statements (if, else, while, for, switch) ################################################# .. code-block:: c /* * (1) The control keyword is followed by a space, a left parenthesis * without a space, the condition, a right parenthesis, a space, and the * opening bracket on the same line. * (2) Always use braces around control statement blocks, even if they only * contain one line. This makes code review diffs smaller if a line gets * added in the future, and avoids the chance of bad indenting making a * line incorrectly appear to be part of the block. * (3) The closing bracket is on a line by itself. */ if (v < 0) { return 0; } /* "else" and "else if" are on the same line with the previous ending brace * and next opening brace, separated by a space. Blank lines may be used * between blocks to help readability. */ if (v > 0) { return 0; } else if (a == 0) { return 1; } else { return 2; } /* Do not use assignments in conditions. This ensures that the developer's * intent is always clear, makes code reviews easier, and reduces the chance * of using assignment where comparison is intended. */ // Do this ... a = f(); if (a) { return 0; } // ... NOT this if (a = f()) { return 0; } /* It helps readability to use the "!" operator only in boolean * comparisons, and explicitly compare numeric values against 0, * pointers against NULL, etc. This helps remind the reader of the * type being compared. */ int i = 0; char *s = NULL; bool cond = false; if (!cond) { return 0; } if (i == 0) { return 0; } if (s == NULL) { return 0; } /* In a "switch" statement, indent "case" one level, and indent the body of * each "case" another level. */ switch (expression) { case 0: command1; break; case 1: command2; break; default: command3; break; } .. index:: pair: C; macro Macros ###### Macros are a powerful but easily misused feature of the C preprocessor, and Pacemaker uses a lot of obscure macro features. If you need to brush up, the `GCC documentation for macros `_ is excellent. Some common issues: * Beware of side effects in macro arguments that may be evaluated more than once * Always parenthesize macro arguments used in the macro body to avoid precedence issues if the argument is an expression * Multi-statement macro bodies should be enclosed in do...while(0) to make them behave more like a single statement and avoid control flow issues Often, a static inline function defined in a header is preferable to a macro, to avoid the numerous issues that plague macros and gain the benefit of argument and return value type checking. .. index:: pair: C; memory Memory Management ################# * Always use ``calloc()`` rather than ``malloc()``. It has no additional cost on modern operating systems, and reduces the severity and security risks of uninitialized memory usage bugs. * Ensure that all dynamically allocated memory is freed when no longer needed, and not used after it is freed. This can be challenging in the more event-driven, callback-oriented sections of code. * Free dynamically allocated memory using the free function corresponding to how it was allocated. For example, use ``free()`` with ``calloc()``, and ``g_free()`` with most glib functions that allocate objects. .. index:: single: C; struct Structures ########## Changes to structures defined in public API headers (adding or removing members, or changing member types) are generally not possible without breaking API compatibility. However, there are exceptions: * Public API structures can be designed such that they can be allocated only via API functions, not declared directly or allocated with standard memory functions using ``sizeof``. * This can be enforced simply by documentating the limitation, in which case new ``struct`` members can be added to the end of the structure without breaking compatibility. * Alternatively, the structure definition can be kept in an internal header, with only a pointer type definition kept in a public header, in which case the structure definition can be changed however needed. .. index:: single: C; variable Variables ######### .. index:: single: C; pointer Pointers ________ .. code-block:: c /* (1) The asterisk goes by the variable name, not the type; * (2) Avoid leaving pointers uninitialized, to lessen the impact of * use-before-assignment bugs */ char *my_string = NULL; // Use space before asterisk and after closing parenthesis in a cast char *foo = (char *) bar; .. index:: single: C; global variable Globals _______ Global variables should be avoided in libraries when possible. State information should instead be passed as function arguments (often as a structure). This is not for thread safety -- Pacemaker's use of forking ensures it will never be threaded -- but it does minimize overhead, improve readability, and avoid obscure side effects. Variable Naming _______________ Time intervals are sometimes represented in Pacemaker code as user-defined text specifications (for example, "10s"), other times as an integer number of seconds or milliseconds, and still other times as a string representation of an integer number. Variables for these should be named with an indication of which is being used (for example, use ``interval_spec``, ``interval_ms``, or ``interval_ms_s`` instead of ``interval``). .. index:: pair: C; booleans pair: C; bool pair: C; gboolean Booleans ________ Booleans in C can be represented by an integer type, ``bool``, or ``gboolean``. Integers are sometimes useful for storing booleans when they must be converted to and from a string, such as an XML attribute value (for which ``crm_element_value_int()`` can be used). Integer booleans use 0 for false and nonzero (usually 1) for true. ``gboolean`` should be used with glib APIs that specify it. ``gboolean`` should always be used with glib's ``TRUE`` and ``FALSE`` constants. Otherwise, ``bool`` should be preferred. ``bool`` should be used with the ``true`` and ``false`` constants from the ``stdbool.h`` header. Do not use equality operators when testing booleans. For example: .. code-block:: c // Do this if (bool1) { fn(); } if (!bool2) { fn2(); } // Not this if (bool1 == true) { fn(); } if (bool2 == false) { fn2(); } // Otherwise there's no logical end ... if ((bool1 == false) == true) { fn(); } .. index:: pair: C; strings String Handling ############### Define Constants for Magic Strings __________________________________ A "magic" string is one used for control purposes rather than human reading, and which must be exactly the same every time it is used. Examples would be configuration option names, XML attribute names, or environment variable names. These should always be defined constants, rather than using the string literal everywhere. If someone mistypes a defined constant, the code won't compile, but if they mistype a literal, it could go unnoticed until a user runs into a problem. String-Related Library Functions ________________________________ Pacemaker's libcrmcommon has a large number of functions to assist in string handling. The most commonly used ones are: * ``pcmk__str_eq()`` tests string equality (similar to ``strcmp()``), but can handle NULL, and takes options for case-insensitive, whether NULL should be considered a match, etc. * ``crm_strdup_printf()`` takes ``printf()``-style arguments and creates a string from them (dynamically allocated, so it must be freed with ``free()``). It asserts on memory failure, so the return value is always non-NULL. String handling functions should almost always be internal API, since Pacemaker isn't intended to be used as a general-purpose library. Most are declared in ``include/crm/common/strings_internal.h``. ``util.h`` has some older ones that are public API (for now, but will eventually be made internal). char*, gchar*, and GString __________________________ When using dynamically allocated strings, be careful to always use the appropriate free function. * ``char*`` strings allocated with something like ``calloc()`` must be freed with ``free()``. Most Pacemaker library functions that allocate strings use this implementation. * glib functions often use ``gchar*`` instead, which must be freed with ``g_free()``. * Occasionally, it's convenient to use glib's flexible ``GString*`` type, which must be freed with ``g_string_free()``. .. index:: pair: C; regular expression Regular Expressions ___________________ - Use ``REG_NOSUB`` with ``regcomp()`` whenever possible, for efficiency. - Be sure to use ``regfree()`` appropriately. .. index:: single: C; enum Enumerations ############ * Enumerations should not have a ``typedef``, and do not require any naming convention beyond what applies to all exposed symbols. * New values should usually be added to the end of public API enumerations, because the compiler will define the values to 0, 1, etc., in the order given, and inserting a value in the middle would change the numerical values of all later values, breaking code compiled with the old values. However, if enum numerical values are explicitly specified rather than left to the compiler, new values can be added anywhere. * When defining constant integer values, enum should be preferred over ``#define`` or ``const`` when possible. This allows type checking without consuming memory. Flag groups ___________ Pacemaker often uses flag groups (also called bit fields or bitmasks) for a collection of boolean options (flags/bits). This is more efficient for storage and manipulation than individual booleans, but its main advantage is when used in public APIs, because using another bit in a bitmask is backward compatible, whereas adding a new function argument (or sometimes even a structure member) is not. .. code-block:: c #include /* (1) Define an enumeration to name the individual flags, for readability. * An enumeration is preferred to a series of "#define" constants * because it is typed, and logically groups the related names. * (2) Define the values using left-shifting, which is more readable and * less error-prone than hexadecimal literals (0x0001, 0x0002, 0x0004, * etc.). * (3) Using a comma after the last entry makes diffs smaller for reviewing * if a new value needs to be added or removed later. */ enum pcmk__some_bitmask_type { pcmk__some_value = (1 << 0), pcmk__other_value = (1 << 1), pcmk__another_value = (1 << 2), }; /* The flag group itself should be an unsigned type from stdint.h (not * the enum type, since it will be a mask of the enum values and not just * one of them). uint32_t is the most common, since we rarely need more than * 32 flags, but a smaller or larger type could be appropriate in some * cases. */ uint32_t flags = pcmk__some_value|pcmk__other_value; /* If the values will be used only with uint64_t, define them accordingly, * to make compilers happier. */ enum pcmk__something_else { pcmk__whatever = (UINT64_C(1) << 0), }; We have convenience functions for checking flags (see ``pcmk_any_flags_set()``, ``pcmk_all_flags_set()``, and ``pcmk_is_set()``) as well as setting and clearing them (see ``pcmk__set_flags_as()`` and ``pcmk__clear_flags_as()``, usually used via wrapper macros defined for specific flag groups). These convenience functions should be preferred to direct bitwise arithmetic, for readability and logging consistency. .. index:: pair: C; function Functions ######### Function Naming _______________ Function names should be unique across the entire project, to allow for individual tracing via ``PCMK_trace_functions``, and make it easier to search code and follow detail logs. .. _sort_func: Sorting ^^^^^^^ A function that sorts an entire list should have ``sort`` in its name. It sorts elements using a :ref:`comparison ` function, which may be either hard-coded or passed as an argument. .. _compare_func: Comparison ^^^^^^^^^^ A comparison function for :ref:`sorting ` should have ``cmp`` in its name and should *not* have ``sort`` in its name. .. _constructor_func: Constructors ^^^^^^^^^^^^ A constructor creates a new dynamically allocated object. It may perform some initialization procedure on the new object. * If the constructor always creates an independent object instance, its name should include ``new``. * If the constructor may add the new object to some existing object, its name should include ``create``. Function Definitions ____________________ .. code-block:: c /* * (1) The return type goes on its own line * (2) The opening brace goes by itself on a line * (3) Use "const" with pointer arguments whenever appropriate, to allow the * function to be used by more callers. */ int my_func1(const char *s) { return 0; } /* Functions with no arguments must explicitly list them as void, * for compatibility with strict compilers */ int my_func2(void) { return 0; } /* * (1) For functions with enough arguments that they must break to the next * line, align arguments with the first argument. * (2) When a function argument is a function itself, use the pointer form. * (3) Declare functions and file-global variables as ``static`` whenever * appropriate. This gains a slight efficiency in shared libraries, and * helps the reader know that it is not used outside the one file. */ static int my_func3(int bar, const char *a, const char *b, const char *c, void (*callback)()) { return 0; } Return Values _____________ Functions that need to indicate success or failure should follow one of the following guidelines. More details, including functions for using them in user messages and converting from one to another, can be found in ``include/crm/common/results.h``. * A **standard Pacemaker return code** is one of the ``pcmk_rc_*`` enum values or a system errno code, as an ``int``. * ``crm_exit_t`` (the ``CRM_EX_*`` enum values) is a system-independent code suitable for the exit status of a process, or for interchange between nodes. * Other special-purpose status codes exist, such as ``enum ocf_exitcode`` for the possible exit statuses of OCF resource agents (along with some Pacemaker-specific extensions). It is usually obvious when the context calls for such. * Some older Pacemaker APIs use the now-deprecated "legacy" return values of ``pcmk_ok`` or the positive or negative value of one of the ``pcmk_err_*`` constants or system errno codes. * Functions registered with external libraries (as callbacks for example) should use the appropriate signature defined by those libraries, rather than follow Pacemaker guidelines. Of course, functions may have return values that aren't success/failure indicators, such as a pointer, integer count, or bool. :ref:`Comparison ` functions should return * a negative integer if the first argument should sort first * 0 if its arguments are equal for sorting purposes * a positive integer is the second argument should sort first Public API Functions ____________________ Unless we are doing a (rare) release where we break public API compatibility, new public API functions can be added, but existing function signatures (return type, name, and argument types) should not be changed. To work around this, an existing function can become a wrapper for a new function. .. index:: pair: C; logging pair: C; output Logging and Output ################## Logging Vs. Output __________________ Log messages and output messages are logically similar but distinct. Oversimplifying a bit, daemons log, and tools output. Log messages are intended to help with troubleshooting and debugging. They may have a high level of technical detail, and are usually filtered by severity -- for example, the system log by default gets messages of notice level and higher. Output is intended to let the user know what a tool is doing, and is generally terser and less technical, and may even be parsed by scripts. Output might have "verbose" and "quiet" modes, but it is not filtered by severity. Common Guidelines for All Messages __________________________________ * When format strings are used for derived data types whose implementation may vary across platforms (``pid_t``, ``time_t``, etc.), the safest approach is to use ``%lld`` in the format string, and cast the value to ``long long``. * Do not rely on ``%s`` handling ``NULL`` values properly. While the standard library functions might, not all functions using printf-style formatting does, and it's safest to get in the habit of always ensuring format values are non-NULL. If a value can be NULL, the ``pcmk__s()`` function is a convenient way to say "this string if not NULL otherwise this default". * The convenience macros ``pcmk__plural_s()`` and ``pcmk__plural_alt()`` are handy when logging a word that may be singular or plural. Log Levels __________ When to use each log level: * **critical:** fatal error (usually something that would make a daemon exit) * **error:** failure of something that affects the cluster (such as a resource action, fencing action, etc.) or daemon operation * **warning:** minor, potential, or recoverable failures (such as something only affecting a daemon client, or invalid configuration that can be left to default) * **notice:** important successful events (such as a node joining or leaving, resource action results, or configuration changes) * **info:** events that would be helpful with troubleshooting (such as status section updates or elections) * **debug:** information that would be helpful for debugging code or complex problems * **trace:** like debug but for very noisy or low-level stuff By default, critical through notice are logged to the system log and detail log, info is logged to the detail log only, and debug and trace are not logged (if enabled, they go to the detail log only). Logging _______ Pacemaker uses libqb for logging, but wraps it with a higher level of functionality (see ``include/crm/common/logging*h``). A few macros ``crm_err()``, ``crm_warn()``, etc. do most of the heavy lifting. By default, Pacemaker sends logs at notice level and higher to the system log, and logs at info level and higher to the detail log (typically ``/var/log/pacemaker/pacemaker.log``). The intent is that most users will only ever need the system log, but for deeper troubleshooting and developer debugging, the detail log may be helpful, at the cost of being more technical and difficult to follow. The same message can have more detail in the detail log than in the system log, using libqb's "extended logging" feature: .. code-block:: c /* The following will log a simple message in the system log, like: warning: Action failed: Node not found with extra detail in the detail log, like: warning: Action failed: Node not found | rc=-1005 id=hgjjg-51006 */ crm_warn("Action failed: %s " QB_XS " rc=%d id=%s", pcmk_rc_str(rc), rc, id); Assertion Logging _________________ ``CRM_ASSERT(expr)`` If ``expr`` is false, this will call crm_err() with a "Triggered fatal assert" message (with details), then abort execution. This should be used for logic errors that should be impossible (such as a NULL function argument where not accepted) and environmental errors that can't be handled gracefully (for example, memory allocation failures, though returning ``ENOMEM`` is often better). ``CRM_LOG_ASSERT(expr)`` If ``expr`` is false, this will generally log a message without aborting. If the log level is below trace, it just calls ``crm_err()`` with a "Triggered assert" message (with details). If the log level is trace, and the caller is a daemon, then it will fork a child process in which to dump core, as well as logging the message. If the log level is trace, and the caller is not a daemon, then it will behave like ``CRM_ASSERT()`` (i.e. log and abort). This should be used for logic or protocol errors that require no special handling. ``CRM_CHECK(expr, failed_action)`` If ``expr`` is false, behave like ``CRM_LOG_ASSERT(expr)`` (that is, log a message and dump core if requested) then perform ``failed_action`` (which must not contain ``continue``, ``break``, or ``errno``). This should be used for logic or protocol errors that can be handled, usually by returning an error status. Output ______ Pacemaker has a somewhat complicated system for tool output. The main benefit is that the user can select the output format with the ``--output-as`` option (usually "text" for human-friendly output or "xml" for reliably script-parsable output, though ``crm_mon`` additionally supports "console" and "html"). A custom message can be defined with a unique string identifier, plus implementation functions for each supported format. The caller invokes the message using the identifier. The user selects the output format via ``--output-as``, and the output code automatically calls the appropriate implementation function. Custom messages are useful when you want to output messages that are more complex than a one-line error or informational message, reproducible, and automatically handled by the output formatting system. Custom messages can contain other custom messages. Custom message functions are implemented as follows: Start with the macro ``PCMK__OUTPUT_ARGS``, whose arguments are the message name, followed by the arguments to the message. Then there is the function declaration, for which the arguments are the pointer to the current output object, then a variable argument list. To output a custom message, you first need to create, i.e. register, the custom message that you want to output. Either call ``register_message``, which registers a custom message at runtime, or make use of the collection of predefined custom messages in ``fmt_functions``, which is defined in ``lib/pacemaker/pcmk_output.c``. Once you have the message to be outputted, output it by calling ``message``. Note: The ``fmt_functions`` functions accommodate all of the output formats; the default implementation accommodates any format that isn't explicitly accommodated. The default output provides valid output for any output format, but you may still want to implement a specific output, i.e. xml, text, or html. The ``message`` function automatically knows which implementation to use, because the ``pcmk__output_s`` contains this information. The interface (most importantly ``pcmk__output_t``) is declared in ``include/crm/common/output*h``. See the API comments and existing tools for examples. Some of its important member functions are ``err``, which formats error messages and ``info``, which formats informational messages. Also, ``list_item``, which formats list items, ``begin_list``, which starts lists, and ``end_list``, which ends lists, are important because lists can be useful, yet differently handled by the different output types. +.. index:: + pair: C; XML + +XML +### + +External Libraries +__________________ + +Pacemaker uses `libxml2 `_ and +`libxslt `_ to process XML. These +libraries implement only version 1.0 of the XML, XPath, and XSLT specifications. + + +Naming +______ + +Names of functions, constants, and enum values related to XML should contain +substrings indicating the type of object they're used with, according to the +following convention: + +* ``xml``: XML subtree, or XML generically +* ``xe``: XML element node, including the attributes belonging to an element +* ``xa``: XML attribute node +* ``xc``: XML comment node + + +Private Data +____________ + +Libxml2 data structures such as ``xmlNode`` and ``xmlDoc`` contain a +``void *_private`` member for application-specific data. Pacemaker uses this +field to store internal bookkeeping data, such as changes relative to another +XML tree, or ACLs. + +XML documents, elements, attributes, and comments have private data. The private +data field must be allocated immediately after the node is created and freed +immediately before the node is freed. + + +Wrapper Functions +_________________ + +Pacemaker provides wrappers for a variety of libxml2 and libxslt functions. They +should be used whenever possible. Some are merely for convenience. However, many +perform additional, Pacemaker-specific tasks, such as change tracking, ACL +checking, and allocation/deallocation of XML documents and private data. + +Pacemaker assumes that every XML node is part of a document and has private data +allocated. If libxml2 APIs are used directly instead of the wrapper functions, +Pacemaker may crash with a segmentation fault, or change tracking and ACL +checking may be incorrectly disabled. + + .. index:: single: Makefile.am Makefiles ######### Pacemaker uses `automake `_ for building, so the Makefile.am in each directory should be edited rather than Makefile.in or Makefile, which are automatically generated. * Public API headers are installed (by adding them to a ``HEADERS`` variable in ``Makefile.am``), but internal API headers are not (by adding them to ``noinst_HEADERS``). .. index:: pair: C; vim settings vim Settings ############ Developers who use ``vim`` to edit source code can add the following settings to their ``~/.vimrc`` file to follow Pacemaker C coding guidelines: .. code-block:: none " follow Pacemaker coding guidelines when editing C source code files filetype plugin indent on au FileType c setlocal expandtab tabstop=4 softtabstop=4 shiftwidth=4 textwidth=80 autocmd BufNewFile,BufRead *.h set filetype=c let c_space_errors = 1 diff --git a/include/crm/common/Makefile.am b/include/crm/common/Makefile.am index b53810f3f3..005bfd62f7 100644 --- a/include/crm/common/Makefile.am +++ b/include/crm/common/Makefile.am @@ -1,47 +1,48 @@ # # Copyright 2004-2024 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. # MAINTAINERCLEANFILES = Makefile.in headerdir=$(pkgincludedir)/crm/common header_HEADERS = acl.h \ actions.h \ agents.h \ cib.h \ ipc.h \ ipc_controld.h \ ipc_pacemakerd.h \ ipc_schedulerd.h \ iso8601.h \ logging.h \ logging_compat.h \ mainloop.h \ nodes.h \ nvpair.h \ + nvpair_compat.h \ options.h \ output.h \ probes.h \ resources.h \ results.h \ roles.h \ rules.h \ scheduler.h \ scheduler_types.h \ schemas.h \ scores.h \ strings.h \ util.h \ util_compat.h \ xml.h \ xml_compat.h \ xml_io.h \ xml_names.h noinst_HEADERS = $(wildcard *internal.h) diff --git a/include/crm/common/nvpair.h b/include/crm/common/nvpair.h index b68ba70d41..c6f9336feb 100644 --- a/include/crm/common/nvpair.h +++ b/include/crm/common/nvpair.h @@ -1,90 +1,86 @@ /* * Copyright 2004-2024 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 PCMK__CRM_COMMON_NVPAIR__H #define PCMK__CRM_COMMON_NVPAIR__H #include // struct timeval #include // gpointer, gboolean, guint, GHashTable #include // xmlNode #include #ifdef __cplusplus extern "C" { #endif /** * \file * \brief Functionality for manipulating name/value pairs * \ingroup core */ typedef struct pcmk_nvpair_s { char *name; char *value; } pcmk_nvpair_t; GSList *pcmk_prepend_nvpair(GSList *nvpairs, const char *name, const char *value); void pcmk_free_nvpairs(GSList *nvpairs); -GSList *pcmk_sort_nvpairs(GSList *list); -GSList *pcmk_xml_attrs2nvpairs(const xmlNode *xml); -void pcmk_nvpairs2xml_attrs(GSList *list, xmlNode *xml); xmlNode *crm_create_nvpair_xml(xmlNode *parent, const char *id, const char *name, const char *value); -void hash2nvpair(gpointer key, gpointer value, gpointer user_data); void hash2field(gpointer key, gpointer value, gpointer user_data); void hash2metafield(gpointer key, gpointer value, gpointer user_data); void hash2smartfield(gpointer key, gpointer value, gpointer user_data); GHashTable *xml2list(const xmlNode *parent); const char *crm_xml_add(xmlNode *node, const char *name, const char *value); const char *crm_xml_add_int(xmlNode *node, const char *name, int value); const char *crm_xml_add_ll(xmlNode *node, const char *name, long long value); const char *crm_xml_add_ms(xmlNode *node, const char *name, guint ms); const char *crm_xml_add_timeval(xmlNode *xml, const char *name_sec, const char *name_usec, const struct timeval *value); const char *crm_element_value(const xmlNode *data, const char *name); int crm_element_value_int(const xmlNode *data, const char *name, int *dest); int crm_element_value_ll(const xmlNode *data, const char *name, long long *dest); int crm_element_value_ms(const xmlNode *data, const char *name, guint *dest); int crm_element_value_epoch(const xmlNode *xml, const char *name, time_t *dest); int crm_element_value_timeval(const xmlNode *data, const char *name_sec, const char *name_usec, struct timeval *dest); char *crm_element_value_copy(const xmlNode *data, const char *name); char *crm_meta_name(const char *field); const char *crm_meta_value(GHashTable *hash, const char *field); /*! * \brief Copy an element from one XML object to another * * \param[in] obj1 Source XML * \param[in,out] obj2 Destination XML * \param[in] element Name of element to copy * * \return Pointer to copied value (from source) */ static inline const char * crm_copy_xml_element(const xmlNode *obj1, xmlNode *obj2, const char *element) { const char *value = crm_element_value(obj1, element); crm_xml_add(obj2, element, value); return value; } #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_NVPAIR__H diff --git a/include/crm/common/nvpair_compat.h b/include/crm/common/nvpair_compat.h new file mode 100644 index 0000000000..3ce56f3cc4 --- /dev/null +++ b/include/crm/common/nvpair_compat.h @@ -0,0 +1,45 @@ +/* + * Copyright 2004-2024 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 PCMK__CRM_COMMON_NVPAIR_COMPAT__H +#define PCMK__CRM_COMMON_NVPAIR_COMPAT__H + +#include // GSList, gpointer +#include // xmlNode + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \file + * \brief Deprecated Pacemaker name-value pair API + * \ingroup core + * \deprecated Do not include this header directly. The nvpair APIs in this + * header, and the header itself, will be removed in a future + * release. + */ + +//! \deprecated Do not use +GSList *pcmk_sort_nvpairs(GSList *list); + +//! \deprecated Do not use +GSList *pcmk_xml_attrs2nvpairs(const xmlNode *xml); + +//! \deprecated Do not use +void pcmk_nvpairs2xml_attrs(GSList *list, xmlNode *xml); + +//! \deprecated Do not use +void hash2nvpair(gpointer key, gpointer value, gpointer user_data); + +#ifdef __cplusplus +} +#endif + +#endif // PCMK__CRM_COMMON_NVPAIR_COMPAT__H diff --git a/include/crm/common/xml_comment_internal.h b/include/crm/common/xml_comment_internal.h new file mode 100644 index 0000000000..59f85591fe --- /dev/null +++ b/include/crm/common/xml_comment_internal.h @@ -0,0 +1,29 @@ +/* + * Copyright 2024 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 PCMK__CRM_COMMON_XML_COMMENT_INTERNAL__H +#define PCMK__CRM_COMMON_XML_COMMENT_INTERNAL__H + +/* + * Internal-only wrappers for and extensions to libxml2 XML comment functions + */ + +#include // xmlDoc, xmlNode + +#ifdef __cplusplus +extern "C" { +#endif + +xmlNode *pcmk__xc_create(xmlDoc *doc, const char *content); + +#ifdef __cplusplus +} +#endif + +#endif // PCMK__XML_COMMENT_INTERNAL__H diff --git a/include/crm/common/xml_internal.h b/include/crm/common/xml_internal.h index 177aa09bed..869df348a0 100644 --- a/include/crm/common/xml_internal.h +++ b/include/crm/common/xml_internal.h @@ -1,612 +1,614 @@ /* * Copyright 2017-2024 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 PCMK__CRM_COMMON_XML_INTERNAL__H #define PCMK__CRM_COMMON_XML_INTERNAL__H /* * Internal-only wrappers for and extensions to libxml2 (libxslt) */ #include #include // uint32_t #include #include #include /* transitively imports qblog.h */ #include #include #include #include // PCMK__XE_PROMOTABLE_LEGACY #include // PCMK_XA_ID, PCMK_XE_CLONE #include #ifdef __cplusplus extern "C" { #endif /*! * \brief Base for directing lib{xml2,xslt} log into standard libqb backend * * This macro implements the core of what can be needed for directing * libxml2 or libxslt error messaging into standard, preconfigured * libqb-backed log stream. * * It's a bit unfortunate that libxml2 (and more sparsely, also libxslt) * emits a single message by chunks (location is emitted separatedly from * the message itself), so we have to take the effort to combine these * chunks back to single message. Whether to do this or not is driven * with \p dechunk toggle. * * The form of a macro was chosen for implicit deriving of __FILE__, etc. * and also because static dechunking buffer should be differentiated per * library (here we assume different functions referring to this macro * will not ever be using both at once), preferably also per-library * context of use to avoid clashes altogether. * * Note that we cannot use qb_logt, because callsite data have to be known * at the moment of compilation, which it is not always the case -- xml_log * (and unfortunately there's no clear explanation of the fail to compile). * * Also note that there's no explicit guard against said libraries producing * never-newline-terminated chunks (which would just keep consuming memory), * as it's quite improbable. Termination of the program in between the * same-message chunks will raise a flag with valgrind and the likes, though. * * And lastly, regarding how dechunking combines with other non-message * parameters -- for \p priority, most important running specification * wins (possibly elevated to LOG_ERR in case of nonconformance with the * newline-termination "protocol"), \p dechunk is expected to always be * on once it was at the start, and the rest (\p postemit and \p prefix) * are picked directly from the last chunk entry finalizing the message * (also reasonable to always have it the same with all related entries). * * \param[in] priority Syslog priority for the message to be logged * \param[in] dechunk Whether to dechunk new-line terminated message * \param[in] postemit Code to be executed once message is sent out * \param[in] prefix How to prefix the message or NULL for raw passing * \param[in] fmt Format string as with printf-like functions * \param[in] ap Variable argument list to supplement \p fmt format string */ #define PCMK__XML_LOG_BASE(priority, dechunk, postemit, prefix, fmt, ap) \ do { \ if (!(dechunk) && (prefix) == NULL) { /* quick pass */ \ qb_log_from_external_source_va(__func__, __FILE__, (fmt), \ (priority), __LINE__, 0, (ap)); \ (void) (postemit); \ } else { \ int CXLB_len = 0; \ char *CXLB_buf = NULL; \ static int CXLB_buffer_len = 0; \ static char *CXLB_buffer = NULL; \ static uint8_t CXLB_priority = 0; \ \ CXLB_len = vasprintf(&CXLB_buf, (fmt), (ap)); \ \ if (CXLB_len <= 0 || CXLB_buf[CXLB_len - 1] == '\n' || !(dechunk)) { \ if (CXLB_len < 0) { \ CXLB_buf = (char *) "LOG CORRUPTION HAZARD"; /*we don't modify*/\ CXLB_priority = QB_MIN(CXLB_priority, LOG_ERR); \ } else if (CXLB_len > 0 /* && (dechunk) */ \ && CXLB_buf[CXLB_len - 1] == '\n') { \ CXLB_buf[CXLB_len - 1] = '\0'; \ } \ if (CXLB_buffer) { \ qb_log_from_external_source(__func__, __FILE__, "%s%s%s", \ CXLB_priority, __LINE__, 0, \ (prefix) != NULL ? (prefix) : "", \ CXLB_buffer, CXLB_buf); \ free(CXLB_buffer); \ } else { \ qb_log_from_external_source(__func__, __FILE__, "%s%s", \ (priority), __LINE__, 0, \ (prefix) != NULL ? (prefix) : "", \ CXLB_buf); \ } \ if (CXLB_len < 0) { \ CXLB_buf = NULL; /* restore temporary override */ \ } \ CXLB_buffer = NULL; \ CXLB_buffer_len = 0; \ (void) (postemit); \ \ } else if (CXLB_buffer == NULL) { \ CXLB_buffer_len = CXLB_len; \ CXLB_buffer = CXLB_buf; \ CXLB_buf = NULL; \ CXLB_priority = (priority); /* remember as a running severest */ \ \ } else { \ CXLB_buffer = realloc(CXLB_buffer, 1 + CXLB_buffer_len + CXLB_len); \ memcpy(CXLB_buffer + CXLB_buffer_len, CXLB_buf, CXLB_len); \ CXLB_buffer_len += CXLB_len; \ CXLB_buffer[CXLB_buffer_len] = '\0'; \ CXLB_priority = QB_MIN(CXLB_priority, (priority)); /* severest? */ \ } \ free(CXLB_buf); \ } \ } while (0) /* * \enum pcmk__xml_fmt_options * \brief Bit flags to control format in XML logs and dumps */ enum pcmk__xml_fmt_options { //! Exclude certain XML attributes (for calculating digests) pcmk__xml_fmt_filtered = (1 << 0), //! Include indentation and newlines pcmk__xml_fmt_pretty = (1 << 1), //! Include the opening tag of an XML element, and include XML comments pcmk__xml_fmt_open = (1 << 3), //! Include the children of an XML element pcmk__xml_fmt_children = (1 << 4), //! Include the closing tag of an XML element pcmk__xml_fmt_close = (1 << 5), // @COMPAT Can we start including text nodes unconditionally? //! Include XML text nodes pcmk__xml_fmt_text = (1 << 6), }; void pcmk__xml_init(void); void pcmk__xml_cleanup(void); int pcmk__xml_show(pcmk__output_t *out, const char *prefix, const xmlNode *data, int depth, uint32_t options); int pcmk__xml_show_changes(pcmk__output_t *out, const xmlNode *xml); /* XML search strings for guest, remote and pacemaker_remote nodes */ /* search string to find CIB resources entries for cluster nodes */ #define PCMK__XP_MEMBER_NODE_CONFIG \ "//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_NODES \ "/" PCMK_XE_NODE \ "[not(@" PCMK_XA_TYPE ") or @" PCMK_XA_TYPE "='" PCMK_VALUE_MEMBER "']" /* search string to find CIB resources entries for guest nodes */ #define PCMK__XP_GUEST_NODE_CONFIG \ "//" PCMK_XE_CIB "//" PCMK_XE_CONFIGURATION "//" PCMK_XE_PRIMITIVE \ "//" PCMK_XE_META_ATTRIBUTES "//" PCMK_XE_NVPAIR \ "[@" PCMK_XA_NAME "='" PCMK_META_REMOTE_NODE "']" /* search string to find CIB resources entries for remote nodes */ #define PCMK__XP_REMOTE_NODE_CONFIG \ "//" PCMK_XE_CIB "//" PCMK_XE_CONFIGURATION "//" PCMK_XE_PRIMITIVE \ "[@" PCMK_XA_TYPE "='" PCMK_VALUE_REMOTE "']" \ "[@" PCMK_XA_PROVIDER "='pacemaker']" /* search string to find CIB node status entries for pacemaker_remote nodes */ #define PCMK__XP_REMOTE_NODE_STATUS \ "//" PCMK_XE_CIB "//" PCMK_XE_STATUS "//" PCMK__XE_NODE_STATE \ "[@" PCMK_XA_REMOTE_NODE "='" PCMK_VALUE_TRUE "']" /*! * \internal * \brief Serialize XML (using libxml) into provided descriptor * * \param[in] fd File descriptor to (piece-wise) write to * \param[in] cur XML subtree to proceed * * \return a standard Pacemaker return code */ int pcmk__xml2fd(int fd, xmlNode *cur); enum pcmk__xml_artefact_ns { pcmk__xml_artefact_ns_legacy_rng = 1, pcmk__xml_artefact_ns_legacy_xslt, pcmk__xml_artefact_ns_base_rng, pcmk__xml_artefact_ns_base_xslt, }; void pcmk__strip_xml_text(xmlNode *xml); const char *pcmk__xe_add_last_written(xmlNode *xe); xmlNode *pcmk__xe_first_child(const xmlNode *parent, const char *node_name, const char *attr_n, const char *attr_v); void pcmk__xe_remove_attr(xmlNode *element, const char *name); bool pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data); void pcmk__xe_remove_matching_attrs(xmlNode *element, bool (*match)(xmlAttrPtr, void *), void *user_data); int pcmk__xe_delete_match(xmlNode *xml, xmlNode *search); int pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace); int pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags); GString *pcmk__element_xpath(const xmlNode *xml); /*! * \internal * \enum pcmk__xml_escape_type * \brief Indicators of which XML characters to escape * * XML allows the escaping of special characters by replacing them with entity * references (for example, """) or character references (for * example, " "). * * The special characters '&' (except as the beginning of an entity * reference) and '<' are not allowed in their literal forms in XML * character data. Character data is non-markup text (for example, the content * of a text node). '>' is allowed under most circumstances; we escape * it for safety and symmetry. * * For more details, see the "Character Data and Markup" section of the XML * spec, currently section 2.4: * https://www.w3.org/TR/xml/#dt-markup * * Attribute values are handled specially. * * If an attribute value is delimited by single quotes, then single quotes * must be escaped within the value. * * Similarly, if an attribute value is delimited by double quotes, then double * quotes must be escaped within the value. * * A conformant XML processor replaces a literal whitespace character (tab, * newline, carriage return, space) in an attribute value with a space * (\c '#x20') character. However, a reference to a whitespace character (for * example, \c " " for \c '\n') does not get replaced. * * For more details, see the "Attribute-Value Normalization" section of the * XML spec, currently section 3.3.3. Note that the default attribute type * is CDATA; we don't deal with NMTOKENS, etc.: * https://www.w3.org/TR/xml/#AVNormalize * * Pacemaker always delimits attribute values with double quotes, so there's no * need to escape single quotes. * * Newlines and tabs should be escaped in attribute values when XML is * serialized to text, so that future parsing preserves them rather than * normalizing them to spaces. * * We always escape carriage returns, so that they're not converted to spaces * during attribute-value normalization and because displaying them as literals * is messy. */ enum pcmk__xml_escape_type { /*! * For text nodes. * * Escape \c '<', \c '>', and \c '&' using entity references. * * Do not escape \c '\n' and \c '\t'. * * Escape other non-printing characters using character references. */ pcmk__xml_escape_text, /*! * For attribute values. * * Escape \c '<', \c '>', \c '&', and \c '"' using entity references. * * Escape \c '\n', \c '\t', and other non-printing characters using * character references. */ pcmk__xml_escape_attr, /* @COMPAT Drop escaping of at least '\n' and '\t' for * pcmk__xml_escape_attr_pretty when openstack-info, openstack-floating-ip, * and openstack-virtual-ip resource agents no longer depend on it. * * At time of writing, openstack-info may set a multiline value for the * openstack_ports node attribute. The other two agents query the value and * require it to be on one line with no spaces. */ /*! * For attribute values displayed in text output delimited by double quotes. * * Escape \c '\n' as \c "\\n" * * Escape \c '\r' as \c "\\r" * * Escape \c '\t' as \c "\\t" * * Escape \c '"' as \c "\\"" */ pcmk__xml_escape_attr_pretty, }; bool pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type); char *pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type); /*! * \internal * \brief Get the root directory to scan XML artefacts of given kind for * * \param[in] ns governs the hierarchy nesting against the inherent root dir * * \return root directory to scan XML artefacts of given kind for */ char * pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns); /*! * \internal * \brief Get the fully unwrapped path to particular XML artifact (RNG/XSLT) * * \param[in] ns denotes path forming details (parent dir, suffix) * \param[in] filespec symbolic file specification to be combined with * #artefact_ns to form the final path * \return unwrapped path to particular XML artifact (RNG/XSLT) */ char *pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec); /*! * \internal * \brief Retrieve the value of the \c PCMK_XA_ID XML attribute * * \param[in] xml XML element to check * * \return Value of the \c PCMK_XA_ID attribute (may be \c NULL) */ static inline const char * pcmk__xe_id(const xmlNode *xml) { return crm_element_value(xml, PCMK_XA_ID); } /*! * \internal * \brief Check whether an XML element is of a particular type * * \param[in] xml XML element to compare * \param[in] name XML element name to compare * * \return \c true if \p xml is of type \p name, otherwise \c false */ static inline bool pcmk__xe_is(const xmlNode *xml, const char *name) { return (xml != NULL) && (xml->name != NULL) && (name != NULL) && (strcmp((const char *) xml->name, name) == 0); } /*! * \internal * \brief Return first non-text child node of an XML node * * \param[in] parent XML node to check * * \return First non-text child node of \p parent (or NULL if none) */ static inline xmlNode * pcmk__xml_first_child(const xmlNode *parent) { xmlNode *child = (parent? parent->children : NULL); while (child && (child->type == XML_TEXT_NODE)) { child = child->next; } return child; } /*! * \internal * \brief Return next non-text sibling node of an XML node * * \param[in] child XML node to check * * \return Next non-text sibling of \p child (or NULL if none) */ static inline xmlNode * pcmk__xml_next(const xmlNode *child) { xmlNode *next = (child? child->next : NULL); while (next && (next->type == XML_TEXT_NODE)) { next = next->next; } return next; } /*! * \internal * \brief Return next non-text sibling element of an XML element * * \param[in] child XML element to check * * \return Next sibling element of \p child (or NULL if none) */ static inline xmlNode * pcmk__xe_next(const xmlNode *child) { xmlNode *next = child? child->next : NULL; while (next && (next->type != XML_ELEMENT_NODE)) { next = next->next; } return next; } xmlNode *pcmk__xe_create(xmlNode *parent, const char *name); +xmlNode *pcmk__xc_create(xmlDoc *doc, const char *content); void pcmk__xml_free(xmlNode *xml); +void pcmk__xml_free_doc(xmlDoc *doc); xmlNode *pcmk__xml_copy(xmlNode *parent, xmlNode *src); xmlNode *pcmk__xe_next_same(const xmlNode *node); void pcmk__xe_set_content(xmlNode *node, const char *format, ...) G_GNUC_PRINTF(2, 3); /*! * \internal * \enum pcmk__xa_flags * \brief Flags for operations affecting XML attributes */ enum pcmk__xa_flags { //! Flag has no effect pcmk__xaf_none = 0U, //! Don't overwrite existing values pcmk__xaf_no_overwrite = (1U << 0), /*! * Treat values as score updates where possible (see * \c pcmk__xe_set_score()) */ pcmk__xaf_score_update = (1U << 1), }; int pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags); void pcmk__xe_sort_attrs(xmlNode *xml); void pcmk__xml_sanitize_id(char *id); void pcmk__xe_set_id(xmlNode *xml, const char *format, ...) G_GNUC_PRINTF(2, 3); /*! * \internal * \brief Like pcmk__xe_set_props, but takes a va_list instead of * arguments directly. * * \param[in,out] node XML to add attributes to * \param[in] pairs NULL-terminated list of name/value pairs to add */ void pcmk__xe_set_propv(xmlNodePtr node, va_list pairs); /*! * \internal * \brief Add a NULL-terminated list of name/value pairs to the given * XML node as properties. * * \param[in,out] node XML node to add properties to * \param[in] ... NULL-terminated list of name/value pairs * * \note A NULL name terminates the arguments; a NULL value will be skipped. */ void pcmk__xe_set_props(xmlNodePtr node, ...) G_GNUC_NULL_TERMINATED; /*! * \internal * \brief Get first attribute of an XML element * * \param[in] xe XML element to check * * \return First attribute of \p xe (or NULL if \p xe is NULL or has none) */ static inline xmlAttr * pcmk__xe_first_attr(const xmlNode *xe) { return (xe == NULL)? NULL : xe->properties; } /*! * \internal * \brief Extract the ID attribute from an XML element * * \param[in] xpath String to search * \param[in] node Node to get the ID for * * \return ID attribute of \p node in xpath string \p xpath */ char * pcmk__xpath_node_id(const char *xpath, const char *node); /*! * \internal * \brief Print an informational message if an xpath query returned multiple * items with the same ID. * * \param[in,out] out The output object * \param[in] search The xpath search result, most typically the result of * calling cib->cmds->query(). * \param[in] name The name searched for */ void pcmk__warn_multiple_name_matches(pcmk__output_t *out, xmlNode *search, const char *name); /* internal XML-related utilities */ enum xml_private_flags { pcmk__xf_none = 0x0000, pcmk__xf_dirty = 0x0001, pcmk__xf_deleted = 0x0002, pcmk__xf_created = 0x0004, pcmk__xf_modified = 0x0008, pcmk__xf_tracking = 0x0010, pcmk__xf_processed = 0x0020, pcmk__xf_skip = 0x0040, pcmk__xf_moved = 0x0080, pcmk__xf_acl_enabled = 0x0100, pcmk__xf_acl_read = 0x0200, pcmk__xf_acl_write = 0x0400, pcmk__xf_acl_deny = 0x0800, pcmk__xf_acl_create = 0x1000, pcmk__xf_acl_denied = 0x2000, pcmk__xf_lazy = 0x4000, }; void pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag); /*! * \internal * \brief Iterate over child elements of \p xml * * This function iterates over the children of \p xml, performing the * callback function \p handler on each node. If the callback returns * a value other than pcmk_rc_ok, the iteration stops and the value is * returned. It is therefore possible that not all children will be * visited. * * \param[in,out] xml The starting XML node. Can be NULL. * \param[in] child_element_name The name that the node must match in order * for \p handler to be run. If NULL, all * child elements will match. * \param[in] handler The callback function. * \param[in,out] userdata User data to pass to the callback function. * Can be NULL. * * \return Standard Pacemaker return code */ int pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name, int (*handler)(xmlNode *xml, void *userdata), void *userdata); bool pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *), void *user_data); static inline const char * pcmk__xml_attr_value(const xmlAttr *attr) { return ((attr == NULL) || (attr->children == NULL))? NULL : (const char *) attr->children->content; } // @COMPAT Drop when PCMK__XE_PROMOTABLE_LEGACY is removed static inline const char * pcmk__map_element_name(const xmlNode *xml) { if (xml == NULL) { return NULL; } else if (pcmk__xe_is(xml, PCMK__XE_PROMOTABLE_LEGACY)) { return PCMK_XE_CLONE; } else { return (const char *) xml->name; } } /*! * \internal * \brief Check whether a given CIB element was modified in a CIB patchset * * \param[in] patchset CIB XML patchset * \param[in] element XML tag of CIB element to check (\c NULL is equivalent * to \c PCMK_XE_CIB). Supported values include any CIB * element supported by \c pcmk__cib_abs_xpath_for(). * * \return \c true if \p element was modified, or \c false otherwise */ bool pcmk__cib_element_in_patchset(const xmlNode *patchset, const char *element); #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_XML_INTERNAL__H diff --git a/include/crm_internal.h b/include/crm_internal.h index 247578b4fb..646b14e3f6 100644 --- a/include/crm_internal.h +++ b/include/crm_internal.h @@ -1,99 +1,100 @@ /* * Copyright 2006-2024 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 PCMK__CRM_INTERNAL__H #define PCMK__CRM_INTERNAL__H #ifndef PCMK__CONFIG_H #define PCMK__CONFIG_H #include #endif #include /* Our minimum glib dependency is 2.42. Define that as both the minimum and * maximum glib APIs that are allowed (i.e. APIs that were already deprecated * in 2.42, and APIs introduced after 2.42, cannot be used by Pacemaker code). */ #define GLIB_VERSION_MIN_REQUIRED GLIB_VERSION_2_42 #define GLIB_VERSION_MAX_ALLOWED GLIB_VERSION_2_42 #include #include #include /* Public API headers can guard including deprecated API headers with this * symbol, thus preventing internal code (which includes this header) from using * deprecated APIs, while still allowing external code to use them by default. */ #define PCMK_ALLOW_DEPRECATED 0 #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #ifdef __cplusplus extern "C" { #endif #define N_(String) (String) #ifdef ENABLE_NLS #define _(String) gettext(String) #else #define _(String) (String) #endif /* * IPC service names that are only used internally */ #define PCMK__SERVER_BASED_RO "cib_ro" #define PCMK__SERVER_BASED_RW "cib_rw" #define PCMK__SERVER_BASED_SHM "cib_shm" /* * IPC commands that can be sent to Pacemaker daemons */ #define PCMK__ATTRD_CMD_PEER_REMOVE "peer-remove" #define PCMK__ATTRD_CMD_UPDATE "update" #define PCMK__ATTRD_CMD_UPDATE_BOTH "update-both" #define PCMK__ATTRD_CMD_UPDATE_DELAY "update-delay" #define PCMK__ATTRD_CMD_QUERY "query" #define PCMK__ATTRD_CMD_REFRESH "refresh" #define PCMK__ATTRD_CMD_SYNC_RESPONSE "sync-response" #define PCMK__ATTRD_CMD_CLEAR_FAILURE "clear-failure" #define PCMK__ATTRD_CMD_CONFIRM "confirm" #define PCMK__CONTROLD_CMD_NODES "list-nodes" #define ST__LEVEL_MIN 1 #define ST__LEVEL_MAX 9 #ifdef __cplusplus } #endif #endif // CRM_INTERNAL__H diff --git a/lib/common/Makefile.am b/lib/common/Makefile.am index 4f6ffa2b71..b2432bac4d 100644 --- a/lib/common/Makefile.am +++ b/lib/common/Makefile.am @@ -1,144 +1,145 @@ # # Copyright 2004-2024 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # include $(top_srcdir)/mk/common.mk AM_CPPFLAGS += -I$(top_builddir)/lib/gnu \ -I$(top_srcdir)/lib/gnu ## libraries lib_LTLIBRARIES = libcrmcommon.la check_LTLIBRARIES = libcrmcommon_test.la # Disable -Wcast-qual if used, because we do some hacky casting, # and because libxml2 has some signatures that should be const but aren't # for backward compatibility reasons. # s390 needs -fPIC # s390-suse-linux/bin/ld: .libs/ipc.o: relocation R_390_PC32DBL against `__stack_chk_fail@@GLIBC_2.4' can not be used when making a shared object; recompile with -fPIC CFLAGS = $(CFLAGS_COPY:-Wcast-qual=) -fPIC # Without "." here, check-recursive will run through the subdirectories first # and then run "make check" here. This will fail, because there's things in # the subdirectories that need check_LTLIBRARIES built first. Adding "." here # changes the order so the subdirectories are processed afterwards. SUBDIRS = . tests noinst_HEADERS = crmcommon_private.h \ mock_private.h libcrmcommon_la_LDFLAGS = -version-info 47:0:13 libcrmcommon_la_CFLAGS = $(CFLAGS_HARDENED_LIB) libcrmcommon_la_LDFLAGS += $(LDFLAGS_HARDENED_LIB) libcrmcommon_la_LIBADD = @LIBADD_DL@ # If configured with --with-profiling or --with-coverage, BUILD_PROFILING will # be set and -fno-builtin will be added to the CFLAGS. However, libcrmcommon # uses the fabs() function which is normally supplied by gcc as one of its # builtins. Therefore we need to explicitly link against libm here or the # tests won't link. if BUILD_PROFILING libcrmcommon_la_LIBADD += -lm endif ## Library sources (*must* use += format for bumplibs) libcrmcommon_la_SOURCES = libcrmcommon_la_SOURCES += acl.c libcrmcommon_la_SOURCES += actions.c libcrmcommon_la_SOURCES += agents.c libcrmcommon_la_SOURCES += alerts.c libcrmcommon_la_SOURCES += attrs.c libcrmcommon_la_SOURCES += cib.c if BUILD_CIBSECRETS libcrmcommon_la_SOURCES += cib_secrets.c endif libcrmcommon_la_SOURCES += cmdline.c libcrmcommon_la_SOURCES += digest.c libcrmcommon_la_SOURCES += health.c libcrmcommon_la_SOURCES += io.c libcrmcommon_la_SOURCES += ipc_attrd.c libcrmcommon_la_SOURCES += ipc_client.c libcrmcommon_la_SOURCES += ipc_common.c libcrmcommon_la_SOURCES += ipc_controld.c libcrmcommon_la_SOURCES += ipc_pacemakerd.c libcrmcommon_la_SOURCES += ipc_schedulerd.c libcrmcommon_la_SOURCES += ipc_server.c libcrmcommon_la_SOURCES += iso8601.c libcrmcommon_la_SOURCES += lists.c libcrmcommon_la_SOURCES += logging.c libcrmcommon_la_SOURCES += mainloop.c libcrmcommon_la_SOURCES += messages.c libcrmcommon_la_SOURCES += nodes.c libcrmcommon_la_SOURCES += nvpair.c libcrmcommon_la_SOURCES += options.c libcrmcommon_la_SOURCES += options_display.c libcrmcommon_la_SOURCES += output.c libcrmcommon_la_SOURCES += output_html.c libcrmcommon_la_SOURCES += output_log.c libcrmcommon_la_SOURCES += output_none.c libcrmcommon_la_SOURCES += output_text.c libcrmcommon_la_SOURCES += output_xml.c libcrmcommon_la_SOURCES += patchset.c libcrmcommon_la_SOURCES += patchset_display.c libcrmcommon_la_SOURCES += pid.c libcrmcommon_la_SOURCES += probes.c libcrmcommon_la_SOURCES += procfs.c libcrmcommon_la_SOURCES += remote.c libcrmcommon_la_SOURCES += resources.c libcrmcommon_la_SOURCES += results.c libcrmcommon_la_SOURCES += roles.c libcrmcommon_la_SOURCES += rules.c libcrmcommon_la_SOURCES += scheduler.c libcrmcommon_la_SOURCES += schemas.c libcrmcommon_la_SOURCES += scores.c libcrmcommon_la_SOURCES += servers.c libcrmcommon_la_SOURCES += strings.c libcrmcommon_la_SOURCES += utils.c libcrmcommon_la_SOURCES += watchdog.c libcrmcommon_la_SOURCES += xml.c libcrmcommon_la_SOURCES += xml_attr.c +libcrmcommon_la_SOURCES += xml_comment.c libcrmcommon_la_SOURCES += xml_display.c libcrmcommon_la_SOURCES += xml_idref.c libcrmcommon_la_SOURCES += xml_io.c libcrmcommon_la_SOURCES += xpath.c # # libcrmcommon_test is used only with unit tests, so we can mock system calls. # See mock.c for details. # include $(top_srcdir)/mk/tap.mk libcrmcommon_test_la_SOURCES = $(libcrmcommon_la_SOURCES) libcrmcommon_test_la_SOURCES += mock.c libcrmcommon_test_la_SOURCES += unittest.c libcrmcommon_test_la_LDFLAGS = $(libcrmcommon_la_LDFLAGS) \ -rpath $(libdir) \ $(LDFLAGS_WRAP) # If GCC emits a builtin function in place of something we've mocked up, that will # get used instead of the mocked version which leads to unexpected test results. So # disable all builtins. Older versions of GCC (at least, on RHEL7) will still emit # replacement code for strdup (and possibly other functions) unless -fno-inline is # also added. libcrmcommon_test_la_CFLAGS = $(libcrmcommon_la_CFLAGS) \ -DPCMK__UNIT_TESTING \ -fno-builtin \ -fno-inline # If -fno-builtin is used, -lm also needs to be added. See the comment at # BUILD_PROFILING above. libcrmcommon_test_la_LIBADD = $(libcrmcommon_la_LIBADD) if BUILD_COVERAGE libcrmcommon_test_la_LIBADD += -lgcov endif libcrmcommon_test_la_LIBADD += -lcmocka libcrmcommon_test_la_LIBADD += -lm nodist_libcrmcommon_test_la_SOURCES = $(nodist_libcrmcommon_la_SOURCES) diff --git a/lib/common/acl.c b/lib/common/acl.c index 33e4b5058c..04d936f615 100644 --- a/lib/common/acl.c +++ b/lib/common/acl.c @@ -1,860 +1,862 @@ /* * Copyright 2004-2024 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" typedef struct xml_acl_s { enum xml_private_flags mode; gchar *xpath; } xml_acl_t; static void free_acl(void *data) { if (data) { xml_acl_t *acl = data; g_free(acl->xpath); free(acl); } } void pcmk__free_acls(GList *acls) { g_list_free_full(acls, free_acl); } static GList * create_acl(const xmlNode *xml, GList *acls, enum xml_private_flags mode) { xml_acl_t *acl = NULL; const char *tag = crm_element_value(xml, PCMK_XA_OBJECT_TYPE); const char *ref = crm_element_value(xml, PCMK_XA_REFERENCE); const char *xpath = crm_element_value(xml, PCMK_XA_XPATH); const char *attr = crm_element_value(xml, PCMK_XA_ATTRIBUTE); if ((tag == NULL) && (ref == NULL) && (xpath == NULL)) { // Schema should prevent this, but to be safe ... crm_trace("Ignoring ACL <%s> element without selection criteria", xml->name); return NULL; } acl = pcmk__assert_alloc(1, sizeof (xml_acl_t)); acl->mode = mode; if (xpath) { acl->xpath = g_strdup(xpath); crm_trace("Unpacked ACL <%s> element using xpath: %s", xml->name, acl->xpath); } else { GString *buf = g_string_sized_new(128); if ((ref != NULL) && (attr != NULL)) { // NOTE: schema currently does not allow this pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" PCMK_XA_ID "='", ref, "' and @", attr, "]", NULL); } else if (ref != NULL) { pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" PCMK_XA_ID "='", ref, "']", NULL); } else if (attr != NULL) { pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@", attr, "]", NULL); } else { pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), NULL); } acl->xpath = buf->str; g_string_free(buf, FALSE); crm_trace("Unpacked ACL <%s> element as xpath: %s", xml->name, acl->xpath); } return g_list_append(acls, acl); } /*! * \internal * \brief Unpack a user, group, or role subtree of the ACLs section * * \param[in] acl_top XML of entire ACLs section * \param[in] acl_entry XML of ACL element being unpacked * \param[in,out] acls List of ACLs unpacked so far * * \return New head of (possibly modified) acls * * \note This function is recursive */ static GList * parse_acl_entry(const xmlNode *acl_top, const xmlNode *acl_entry, GList *acls) { for (const xmlNode *child = pcmk__xe_first_child(acl_entry, NULL, NULL, NULL); child != NULL; child = pcmk__xe_next(child)) { if (pcmk__xe_is(child, PCMK_XE_ACL_PERMISSION)) { const char *kind = crm_element_value(child, PCMK_XA_KIND); CRM_ASSERT(kind != NULL); crm_trace("Unpacking <" PCMK_XE_ACL_PERMISSION "> element of " "kind '%s'", kind); if (pcmk__str_eq(kind, PCMK_VALUE_READ, pcmk__str_none)) { acls = create_acl(child, acls, pcmk__xf_acl_read); } else if (pcmk__str_eq(kind, PCMK_VALUE_WRITE, pcmk__str_none)) { acls = create_acl(child, acls, pcmk__xf_acl_write); } else if (pcmk__str_eq(kind, PCMK_VALUE_DENY, pcmk__str_none)) { acls = create_acl(child, acls, pcmk__xf_acl_deny); } else { crm_warn("Ignoring unknown ACL kind '%s'", kind); } } else if (pcmk__xe_is(child, PCMK_XE_ROLE)) { const char *ref_role = crm_element_value(child, PCMK_XA_ID); crm_trace("Unpacking <" PCMK_XE_ROLE "> element"); if (ref_role == NULL) { continue; } for (xmlNode *role = pcmk__xe_first_child(acl_top, NULL, NULL, NULL); role != NULL; role = pcmk__xe_next(role)) { const char *role_id = NULL; if (!pcmk__xe_is(role, PCMK_XE_ACL_ROLE)) { continue; } role_id = crm_element_value(role, PCMK_XA_ID); if (pcmk__str_eq(ref_role, role_id, pcmk__str_none)) { crm_trace("Unpacking referenced role '%s' in <%s> element", role_id, acl_entry->name); acls = parse_acl_entry(acl_top, role, acls); break; } } } } return acls; } /* */ static const char * acl_to_text(enum xml_private_flags flags) { if (pcmk_is_set(flags, pcmk__xf_acl_deny)) { return "deny"; } else if (pcmk_any_flags_set(flags, pcmk__xf_acl_write|pcmk__xf_acl_create)) { return "read/write"; } else if (pcmk_is_set(flags, pcmk__xf_acl_read)) { return "read"; } return "none"; } void pcmk__apply_acl(xmlNode *xml) { GList *aIter = NULL; xml_doc_private_t *docpriv = xml->doc->_private; xml_node_private_t *nodepriv; xmlXPathObjectPtr xpathObj = NULL; if (!xml_acl_enabled(xml)) { crm_trace("Skipping ACLs for user '%s' because not enabled for this XML", docpriv->user); return; } for (aIter = docpriv->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); nodepriv = match->_private; pcmk__set_xml_flags(nodepriv, acl->mode); // Build a GString only if tracing is enabled pcmk__if_tracing( { GString *path = pcmk__element_xpath(match); crm_trace("Applying %s ACL to %s matched by %s", acl_to_text(acl->mode), path->str, acl->xpath); g_string_free(path, TRUE); }, {} ); } crm_trace("Applied %s ACL %s (%d match%s)", acl_to_text(acl->mode), acl->xpath, max, ((max == 1)? "" : "es")); freeXpathObject(xpathObj); } } /*! * \internal * \brief Unpack ACLs for a given user into the * metadata of the target XML tree * * Taking the description of ACLs from the source XML tree and * marking up the target XML tree with access information for the * given user by tacking it onto the relevant nodes * * \param[in] source XML with ACL definitions * \param[in,out] target XML that ACLs will be applied to * \param[in] user Username whose ACLs need to be unpacked */ void pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user) { xml_doc_private_t *docpriv = NULL; if ((target == NULL) || (target->doc == NULL) || (target->doc->_private == NULL)) { return; } docpriv = target->doc->_private; if (!pcmk_acl_required(user)) { crm_trace("Not unpacking ACLs because not required for user '%s'", user); } else if (docpriv->acls == NULL) { xmlNode *acls = get_xpath_object("//" PCMK_XE_ACLS, source, LOG_NEVER); pcmk__str_update(&docpriv->user, user); if (acls) { xmlNode *child = NULL; for (child = pcmk__xe_first_child(acls, NULL, NULL, NULL); child != NULL; child = pcmk__xe_next(child)) { if (pcmk__xe_is(child, PCMK_XE_ACL_TARGET)) { const char *id = crm_element_value(child, PCMK_XA_NAME); if (id == NULL) { id = crm_element_value(child, PCMK_XA_ID); } if (id && strcmp(id, user) == 0) { crm_debug("Unpacking ACLs for user '%s'", id); docpriv->acls = parse_acl_entry(acls, child, docpriv->acls); } } else if (pcmk__xe_is(child, PCMK_XE_ACL_GROUP)) { const char *id = crm_element_value(child, PCMK_XA_NAME); if (id == NULL) { id = crm_element_value(child, PCMK_XA_ID); } if (id && pcmk__is_user_in_group(user,id)) { crm_debug("Unpacking ACLs for group '%s'", id); docpriv->acls = parse_acl_entry(acls, child, docpriv->acls); } } } } } } /*! * \internal * \brief Copy source to target and set xf_acl_enabled flag in target * * \param[in] acl_source XML with ACL definitions * \param[in,out] target XML that ACLs will be applied to * \param[in] user Username whose ACLs need to be set */ void pcmk__enable_acl(xmlNode *acl_source, xmlNode *target, const char *user) { pcmk__unpack_acl(acl_source, target, user); pcmk__set_xml_doc_flag(target, pcmk__xf_acl_enabled); pcmk__apply_acl(target); } static inline bool test_acl_mode(enum xml_private_flags allowed, enum xml_private_flags requested) { if (pcmk_is_set(allowed, pcmk__xf_acl_deny)) { return false; } else if (pcmk_all_flags_set(allowed, requested)) { return true; } else if (pcmk_is_set(requested, pcmk__xf_acl_read) && pcmk_is_set(allowed, pcmk__xf_acl_write)) { return true; } else if (pcmk_is_set(requested, pcmk__xf_acl_create) && pcmk_any_flags_set(allowed, pcmk__xf_acl_write|pcmk__xf_created)) { return true; } return false; } /*! * \internal * \brief Rid XML tree of all unreadable nodes and node properties * * \param[in,out] xml Root XML node to be purged of attributes * * \return true if this node or any of its children are readable * if false is returned, xml will be freed * * \note This function is recursive */ static bool purge_xml_attributes(xmlNode *xml) { xmlNode *child = NULL; xmlAttr *xIter = NULL; bool readable_children = false; xml_node_private_t *nodepriv = xml->_private; if (test_acl_mode(nodepriv->flags, pcmk__xf_acl_read)) { crm_trace("%s[@" PCMK_XA_ID "=%s] is readable", xml->name, pcmk__xe_id(xml)); return true; } xIter = xml->properties; while (xIter != NULL) { xmlAttr *tmp = xIter; const char *prop_name = (const char *)xIter->name; xIter = xIter->next; if (strcmp(prop_name, PCMK_XA_ID) == 0) { continue; } - xmlUnsetProp(xml, tmp->name); + pcmk__xa_remove(tmp, true); } child = pcmk__xml_first_child(xml); while ( child != NULL ) { xmlNode *tmp = child; child = pcmk__xml_next(child); readable_children |= purge_xml_attributes(tmp); } if (!readable_children) { // Nothing readable under here, so purge completely pcmk__xml_free(xml); } return readable_children; } /*! * \brief Copy ACL-allowed portions of specified XML * * \param[in] user Username whose ACLs should be used * \param[in] acl_source XML containing ACLs * \param[in] xml XML to be copied * \param[out] result Copy of XML portions readable via ACLs * * \return true if xml exists and ACLs are required for user, false otherwise * \note If this returns true, caller should use \p result rather than \p xml */ bool xml_acl_filtered_copy(const char *user, xmlNode *acl_source, xmlNode *xml, xmlNode **result) { GList *aIter = NULL; xmlNode *target = NULL; xml_doc_private_t *docpriv = NULL; *result = NULL; if ((xml == NULL) || !pcmk_acl_required(user)) { crm_trace("Not filtering XML because ACLs not required for user '%s'", user); return false; } crm_trace("Filtering XML copy using user '%s' ACLs", user); target = pcmk__xml_copy(NULL, xml); if (target == NULL) { return true; } pcmk__enable_acl(acl_source, target, user); docpriv = target->doc->_private; for(aIter = docpriv->acls; aIter != NULL && target; aIter = aIter->next) { int max = 0; xml_acl_t *acl = aIter->data; if (acl->mode != pcmk__xf_acl_deny) { /* Nothing to do */ } else if (acl->xpath) { int lpc = 0; xmlXPathObjectPtr xpathObj = xpath_search(target, acl->xpath); max = numXpathResults(xpathObj); for(lpc = 0; lpc < max; lpc++) { xmlNode *match = getXpathResult(xpathObj, lpc); if (!purge_xml_attributes(match) && (match == target)) { crm_trace("ACLs deny user '%s' access to entire XML document", user); freeXpathObject(xpathObj); return true; } } crm_trace("ACLs deny user '%s' access to %s (%d %s)", user, acl->xpath, max, pcmk__plural_alt(max, "match", "matches")); freeXpathObject(xpathObj); } } if (!purge_xml_attributes(target)) { crm_trace("ACLs deny user '%s' access to entire XML document", user); return true; } if (docpriv->acls) { g_list_free_full(docpriv->acls, free_acl); docpriv->acls = NULL; } else { crm_trace("User '%s' without ACLs denied access to entire XML document", user); pcmk__xml_free(target); target = NULL; } if (target) { *result = target; } return true; } /*! * \internal * \brief Check whether creation of an XML element is implicitly allowed * * Check whether XML is a "scaffolding" element whose creation is implicitly * allowed regardless of ACLs (that is, it is not in the ACL section and has * no attributes other than \c PCMK_XA_ID). * * \param[in] xml XML element to check * * \return true if XML element is implicitly allowed, false otherwise */ static bool implicitly_allowed(const xmlNode *xml) { GString *path = NULL; for (xmlAttr *prop = xml->properties; prop != NULL; prop = prop->next) { if (strcmp((const char *) prop->name, PCMK_XA_ID) != 0) { return false; } } path = pcmk__element_xpath(xml); CRM_ASSERT(path != NULL); if (strstr((const char *) path->str, "/" PCMK_XE_ACLS "/") != NULL) { g_string_free(path, TRUE); return false; } g_string_free(path, TRUE); return true; } #define display_id(xml) pcmk__s(pcmk__xe_id(xml), "") /*! * \internal * \brief Drop XML nodes created in violation of ACLs * * Given an XML element, free all of its descendant nodes created in violation * of ACLs, with the exception of allowing "scaffolding" elements (i.e. those * that aren't in the ACL section and don't have any attributes other than * \c PCMK_XA_ID). * * \param[in,out] xml XML to check * \param[in] check_top Whether to apply checks to argument itself * (if true, xml might get freed) * * \note This function is recursive */ void pcmk__apply_creation_acl(xmlNode *xml, bool check_top) { xml_node_private_t *nodepriv = xml->_private; if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { if (implicitly_allowed(xml)) { crm_trace("Creation of <%s> scaffolding with " PCMK_XA_ID "=\"%s\"" " is implicitly allowed", xml->name, display_id(xml)); } else if (pcmk__check_acl(xml, NULL, pcmk__xf_acl_write)) { crm_trace("ACLs allow creation of <%s> with " PCMK_XA_ID "=\"%s\"", xml->name, display_id(xml)); } else if (check_top) { /* is_root=true should be impossible with check_top=true, but check * for sanity */ - bool is_root = (xml->doc != NULL) - && (xmlDocGetRootElement(xml->doc) == xml); + bool is_root = (xmlDocGetRootElement(xml->doc) == xml); + xml_doc_private_t *docpriv = xml->doc->_private; crm_trace("ACLs disallow creation of %s<%s> with " PCMK_XA_ID "=\"%s\"", (is_root? "root element " : ""), xml->name, display_id(xml)); - if (is_root) { - xmlFreeDoc(xml->doc); - } else { - xmlUnlinkNode(xml); - xmlFreeNode(xml); + // pcmk__xml_free() checks ACLs if enabled, which would fail + pcmk__clear_xml_flags(docpriv, pcmk__xf_acl_enabled); + pcmk__xml_free(xml); + + if (!is_root) { + // If root, the document was freed. Otherwise re-enable ACLs. + pcmk__set_xml_flags(docpriv, pcmk__xf_acl_enabled); } return; } else { crm_notice("ACLs would disallow creation of %s<%s> with " PCMK_XA_ID "=\"%s\"", ((xml == xmlDocGetRootElement(xml->doc))? "root element " : ""), xml->name, display_id(xml)); } } for (xmlNode *cIter = pcmk__xml_first_child(xml); cIter != NULL; ) { xmlNode *child = cIter; cIter = pcmk__xml_next(cIter); /* In case it is free'd */ pcmk__apply_creation_acl(child, true); } } /*! * \brief Check whether or not an XML node is ACL-denied * * \param[in] xml node to check * * \return true if XML node exists and is ACL-denied, false otherwise */ bool xml_acl_denied(const xmlNode *xml) { if (xml && xml->doc && xml->doc->_private){ xml_doc_private_t *docpriv = xml->doc->_private; return pcmk_is_set(docpriv->flags, pcmk__xf_acl_denied); } return false; } void xml_acl_disable(xmlNode *xml) { if (xml_acl_enabled(xml)) { xml_doc_private_t *docpriv = xml->doc->_private; /* Catch anything that was created but shouldn't have been */ pcmk__apply_acl(xml); pcmk__apply_creation_acl(xml, false); pcmk__clear_xml_flags(docpriv, pcmk__xf_acl_enabled); } } /*! * \brief Check whether or not an XML node is ACL-enabled * * \param[in] xml node to check * * \return true if XML node exists and is ACL-enabled, false otherwise */ bool xml_acl_enabled(const xmlNode *xml) { if (xml && xml->doc && xml->doc->_private){ xml_doc_private_t *docpriv = xml->doc->_private; return pcmk_is_set(docpriv->flags, pcmk__xf_acl_enabled); } return false; } 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 (pcmk__tracking_xml_changes(xml, false) && xml_acl_enabled(xml)) { xmlNode *parent = xml; xml_doc_private_t *docpriv = xml->doc->_private; GString *xpath = NULL; if (docpriv->acls == NULL) { pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_denied); pcmk__if_tracing({}, return false); xpath = pcmk__element_xpath(xml); if (name != NULL) { pcmk__g_strcat(xpath, "[@", name, "]", NULL); } qb_log_from_external_source(__func__, __FILE__, "User '%s' without ACLs denied %s " "access to %s", LOG_TRACE, __LINE__, 0, docpriv->user, acl_to_text(mode), (const char *) xpath->str); g_string_free(xpath, TRUE); 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 == pcmk__xf_acl_create) { mode = pcmk__xf_acl_write; } } while (parent && parent->_private) { xml_node_private_t *nodepriv = parent->_private; if (test_acl_mode(nodepriv->flags, mode)) { return true; } else if (pcmk_is_set(nodepriv->flags, pcmk__xf_acl_deny)) { pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_denied); pcmk__if_tracing({}, return false); xpath = pcmk__element_xpath(xml); if (name != NULL) { pcmk__g_strcat(xpath, "[@", name, "]", NULL); } qb_log_from_external_source(__func__, __FILE__, "%sACL denies user '%s' %s access " "to %s", LOG_TRACE, __LINE__, 0, (parent != xml)? "Parent ": "", docpriv->user, acl_to_text(mode), (const char *) xpath->str); g_string_free(xpath, TRUE); return false; } parent = parent->parent; } pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_denied); pcmk__if_tracing({}, return false); xpath = pcmk__element_xpath(xml); if (name != NULL) { pcmk__g_strcat(xpath, "[@", name, "]", NULL); } qb_log_from_external_source(__func__, __FILE__, "Default ACL denies user '%s' %s access to " "%s", LOG_TRACE, __LINE__, 0, docpriv->user, acl_to_text(mode), (const char *) xpath->str); g_string_free(xpath, TRUE); return false; } return true; } /*! * \brief Check whether ACLs are required for a given user * * \param[in] User name to check * * \return true if the user requires ACLs, false otherwise */ bool pcmk_acl_required(const char *user) { if (pcmk__str_empty(user)) { crm_trace("ACLs not required because no user set"); return false; } else if (!strcmp(user, CRM_DAEMON_USER) || !strcmp(user, "root")) { crm_trace("ACLs not required for privileged user %s", user); return false; } crm_trace("ACLs required for %s", user); return true; } char * pcmk__uid2username(uid_t uid) { struct passwd *pwent = getpwuid(uid); if (pwent == NULL) { crm_perror(LOG_INFO, "Cannot get user details for user ID %d", uid); return NULL; } return pcmk__str_copy(pwent->pw_name); } /*! * \internal * \brief Set the ACL user field properly on an XML request * * Multiple user names are potentially involved in an XML request: the effective * user of the current process; the user name known from an IPC client * connection; and the user name obtained from the request itself, whether by * the current standard XML attribute name or an older legacy attribute name. * This function chooses the appropriate one that should be used for ACLs, sets * it in the request (using the standard attribute name, and the legacy name if * given), and returns it. * * \param[in,out] request XML request to update * \param[in] field Alternate name for ACL user name XML attribute * \param[in] peer_user User name as known from IPC connection * * \return ACL user name actually used */ const char * pcmk__update_acl_user(xmlNode *request, const char *field, const char *peer_user) { static const char *effective_user = NULL; const char *requested_user = NULL; const char *user = NULL; if (effective_user == NULL) { effective_user = pcmk__uid2username(geteuid()); if (effective_user == NULL) { effective_user = pcmk__str_copy("#unprivileged"); crm_err("Unable to determine effective user, assuming unprivileged for ACLs"); } } requested_user = crm_element_value(request, PCMK__XA_ACL_TARGET); if (requested_user == NULL) { /* @COMPAT rolling upgrades <=1.1.11 * * field is checked for backward compatibility with older versions that * did not use PCMK__XA_ACL_TARGET. */ requested_user = crm_element_value(request, field); } if (!pcmk__is_privileged(effective_user)) { /* We're not running as a privileged user, set or overwrite any existing * value for PCMK__XA_ACL_TARGET */ user = effective_user; } else if (peer_user == NULL && requested_user == NULL) { /* No user known or requested, use 'effective_user' and make sure one is * set for the request */ user = effective_user; } else if (peer_user == NULL) { /* No user known, trusting 'requested_user' */ user = requested_user; } else if (!pcmk__is_privileged(peer_user)) { /* The peer is not a privileged user, set or overwrite any existing * value for PCMK__XA_ACL_TARGET */ user = peer_user; } else if (requested_user == NULL) { /* Even if we're privileged, make sure there is always a value set */ user = peer_user; } else { /* Legal delegation to 'requested_user' */ user = requested_user; } // This requires pointer comparison, not string comparison if (user != crm_element_value(request, PCMK__XA_ACL_TARGET)) { crm_xml_add(request, PCMK__XA_ACL_TARGET, user); } if (field != NULL && user != crm_element_value(request, field)) { crm_xml_add(request, field, user); } return requested_user; } diff --git a/lib/common/crmcommon_private.h b/lib/common/crmcommon_private.h index a100534e20..74439ff712 100644 --- a/lib/common/crmcommon_private.h +++ b/lib/common/crmcommon_private.h @@ -1,446 +1,458 @@ /* * Copyright 2018-2024 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 PCMK__COMMON_CRMCOMMON_PRIVATE__H #define PCMK__COMMON_CRMCOMMON_PRIVATE__H /* This header is for the sole use of libcrmcommon, so that functions can be * declared with G_GNUC_INTERNAL for efficiency. */ #include // uint8_t, uint32_t #include // bool #include // size_t #include // G_GNUC_INTERNAL, G_GNUC_PRINTF, gchar, etc. #include // xmlNode, xmlAttr #include // struct qb_ipc_response_header #include // pcmk_ipc_api_t, crm_ipc_t, etc. #include // crm_time_t #include // LOG_NEVER #include // mainloop_io_t #include // pcmk__output_t #include // crm_exit_t #include // pcmk_rule_input_t #include // pcmkXmlStr #include // enum xml_private_flags #ifdef __cplusplus extern "C" { #endif // Decent chunk size for processing large amounts of data #define PCMK__BUFFER_SIZE 4096 #if defined(PCMK__UNIT_TESTING) #undef G_GNUC_INTERNAL #define G_GNUC_INTERNAL #endif /* When deleting portions of an XML tree, we keep a record so we can know later * (e.g. when checking differences) that something was deleted. */ typedef struct pcmk__deleted_xml_s { gchar *path; int position; } pcmk__deleted_xml_t; typedef struct xml_node_private_s { uint32_t check; uint32_t flags; } xml_node_private_t; typedef struct xml_doc_private_s { uint32_t check; uint32_t flags; char *user; GList *acls; GList *deleted_objs; // List of pcmk__deleted_xml_t } xml_doc_private_t; -// XML entity references +// XML private data magic numbers +#define PCMK__XML_DOC_PRIVATE_MAGIC 0x81726354UL +#define PCMK__XML_NODE_PRIVATE_MAGIC 0x54637281UL +// XML entity references #define PCMK__XML_ENTITY_AMP "&" #define PCMK__XML_ENTITY_GT ">" #define PCMK__XML_ENTITY_LT "<" #define PCMK__XML_ENTITY_QUOT """ -//! libxml2 supports only XML version 1.0, at least as of libxml2-2.12.5 -#define PCMK__XML_VERSION ((pcmkXmlStr) "1.0") - #define pcmk__set_xml_flags(xml_priv, flags_to_set) do { \ (xml_priv)->flags = pcmk__set_flags_as(__func__, __LINE__, \ LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \ (flags_to_set), #flags_to_set); \ } while (0) #define pcmk__clear_xml_flags(xml_priv, flags_to_clear) do { \ (xml_priv)->flags = pcmk__clear_flags_as(__func__, __LINE__, \ LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \ (flags_to_clear), #flags_to_clear); \ } while (0) G_GNUC_INTERNAL -bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy); +void pcmk__xml_set_parent_flags(xmlNode *xml, uint64_t flags); G_GNUC_INTERNAL -void pcmk__xml_mark_created(xmlNode *xml); +void pcmk__xml_new_private_data(xmlNode *xml); + +G_GNUC_INTERNAL +void pcmk__xml_free_private_data(xmlNode *xml); + +G_GNUC_INTERNAL +xmlDoc *pcmk__xml_new_doc(void); + +G_GNUC_INTERNAL +bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy); G_GNUC_INTERNAL int pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set); G_GNUC_INTERNAL xmlNode *pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact); G_GNUC_INTERNAL xmlNode *pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, bool exact); G_GNUC_INTERNAL void pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update); 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__is_user_in_group(const char *user, const char *group); G_GNUC_INTERNAL void pcmk__apply_acl(xmlNode *xml); G_GNUC_INTERNAL void pcmk__apply_creation_acl(xmlNode *xml, bool check_top); +G_GNUC_INTERNAL +int pcmk__xa_remove(xmlAttr *attr, bool force); + G_GNUC_INTERNAL void pcmk__mark_xml_attr_dirty(xmlAttr *a); G_GNUC_INTERNAL bool pcmk__xa_filterable(const char *name); G_GNUC_INTERNAL void pcmk__log_xmllib_err(void *ctx, const char *fmt, ...) G_GNUC_PRINTF(2, 3); G_GNUC_INTERNAL void pcmk__mark_xml_node_dirty(xmlNode *xml); G_GNUC_INTERNAL bool pcmk__marked_as_deleted(xmlAttrPtr a, void *user_data); G_GNUC_INTERNAL void pcmk__dump_xml_attr(const xmlAttr *attr, GString *buffer); G_GNUC_INTERNAL int pcmk__xe_set_score(xmlNode *target, const char *name, const char *value); G_GNUC_INTERNAL bool pcmk__xml_is_name_start_char(const char *utf8, int *len); G_GNUC_INTERNAL bool pcmk__xml_is_name_char(const char *utf8, int *len); /* * Date/times */ // For use with pcmk__add_time_from_xml() enum pcmk__time_component { pcmk__time_unknown, pcmk__time_years, pcmk__time_months, pcmk__time_weeks, pcmk__time_days, pcmk__time_hours, pcmk__time_minutes, pcmk__time_seconds, }; G_GNUC_INTERNAL const char *pcmk__time_component_attr(enum pcmk__time_component component); G_GNUC_INTERNAL int pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component, const xmlNode *xml); G_GNUC_INTERNAL void pcmk__set_time_if_earlier(crm_time_t *target, const crm_time_t *source); /* * IPC */ #define PCMK__IPC_VERSION 1 #define PCMK__CONTROLD_API_MAJOR "1" #define PCMK__CONTROLD_API_MINOR "0" // IPC behavior that varies by daemon typedef struct pcmk__ipc_methods_s { /*! * \internal * \brief Allocate any private data needed by daemon IPC * * \param[in,out] api IPC API connection * * \return Standard Pacemaker return code */ int (*new_data)(pcmk_ipc_api_t *api); /*! * \internal * \brief Free any private data used by daemon IPC * * \param[in,out] api_data Data allocated by new_data() method */ void (*free_data)(void *api_data); /*! * \internal * \brief Perform daemon-specific handling after successful connection * * Some daemons require clients to register before sending any other * commands. The controller requires a CRM_OP_HELLO (with no reply), and * the CIB manager, executor, and fencer require a CRM_OP_REGISTER (with a * reply). Ideally this would be consistent across all daemons, but for now * this allows each to do its own authorization. * * \param[in,out] api IPC API connection * * \return Standard Pacemaker return code */ int (*post_connect)(pcmk_ipc_api_t *api); /*! * \internal * \brief Check whether an IPC request results in a reply * * \param[in,out] api IPC API connection * \param[in] request IPC request XML * * \return true if request would result in an IPC reply, false otherwise */ bool (*reply_expected)(pcmk_ipc_api_t *api, const xmlNode *request); /*! * \internal * \brief Perform daemon-specific handling of an IPC message * * \param[in,out] api IPC API connection * \param[in,out] msg Message read from IPC connection * * \return true if more IPC reply messages should be expected */ bool (*dispatch)(pcmk_ipc_api_t *api, xmlNode *msg); /*! * \internal * \brief Perform daemon-specific handling of an IPC disconnect * * \param[in,out] api IPC API connection */ void (*post_disconnect)(pcmk_ipc_api_t *api); } pcmk__ipc_methods_t; // Implementation of pcmk_ipc_api_t struct pcmk_ipc_api_s { enum pcmk_ipc_server server; // Daemon this IPC API instance is for enum pcmk_ipc_dispatch dispatch_type; // How replies should be dispatched size_t ipc_size_max; // maximum IPC buffer size crm_ipc_t *ipc; // IPC connection mainloop_io_t *mainloop_io; // If using mainloop, I/O source for IPC bool free_on_disconnect; // Whether disconnect should free object pcmk_ipc_callback_t cb; // Caller-registered callback (if any) void *user_data; // Caller-registered data (if any) void *api_data; // For daemon-specific use pcmk__ipc_methods_t *cmds; // Behavior that varies by daemon }; typedef struct pcmk__ipc_header_s { struct qb_ipc_response_header qb; uint32_t size_uncompressed; uint32_t size_compressed; uint32_t flags; uint8_t version; } pcmk__ipc_header_t; G_GNUC_INTERNAL int pcmk__send_ipc_request(pcmk_ipc_api_t *api, const xmlNode *request); G_GNUC_INTERNAL void pcmk__call_ipc_callback(pcmk_ipc_api_t *api, enum pcmk_ipc_event event_type, crm_exit_t status, void *event_data); G_GNUC_INTERNAL unsigned int pcmk__ipc_buffer_size(unsigned int max); G_GNUC_INTERNAL bool pcmk__valid_ipc_header(const pcmk__ipc_header_t *header); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__attrd_api_methods(void); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__controld_api_methods(void); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__pacemakerd_api_methods(void); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__schedulerd_api_methods(void); /* * Logging */ //! XML is newly created #define PCMK__XML_PREFIX_CREATED "++" //! XML has been deleted #define PCMK__XML_PREFIX_DELETED "--" //! XML has been modified #define PCMK__XML_PREFIX_MODIFIED "+ " //! XML has been moved #define PCMK__XML_PREFIX_MOVED "+~" /* * Output */ G_GNUC_INTERNAL int pcmk__bare_output_new(pcmk__output_t **out, const char *fmt_name, const char *filename, char **argv); G_GNUC_INTERNAL void pcmk__register_option_messages(pcmk__output_t *out); G_GNUC_INTERNAL void pcmk__register_patchset_messages(pcmk__output_t *out); G_GNUC_INTERNAL bool pcmk__output_text_get_fancy(pcmk__output_t *out); /* * Rules */ // How node attribute values may be compared in rules enum pcmk__comparison { pcmk__comparison_unknown, pcmk__comparison_defined, pcmk__comparison_undefined, pcmk__comparison_eq, pcmk__comparison_ne, pcmk__comparison_lt, pcmk__comparison_lte, pcmk__comparison_gt, pcmk__comparison_gte, }; // How node attribute values may be parsed in rules enum pcmk__type { pcmk__type_unknown, pcmk__type_string, pcmk__type_integer, pcmk__type_number, pcmk__type_version, }; // Where to obtain reference value for a node attribute comparison enum pcmk__reference_source { pcmk__source_unknown, pcmk__source_literal, pcmk__source_instance_attrs, pcmk__source_meta_attrs, }; G_GNUC_INTERNAL enum pcmk__comparison pcmk__parse_comparison(const char *op); G_GNUC_INTERNAL enum pcmk__type pcmk__parse_type(const char *type, enum pcmk__comparison op, const char *value1, const char *value2); G_GNUC_INTERNAL enum pcmk__reference_source pcmk__parse_source(const char *source); G_GNUC_INTERNAL int pcmk__cmp_by_type(const char *value1, const char *value2, enum pcmk__type type); G_GNUC_INTERNAL int pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start, crm_time_t **end); G_GNUC_INTERNAL int pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now); G_GNUC_INTERNAL int pcmk__evaluate_attr_expression(const xmlNode *expression, const pcmk_rule_input_t *rule_input); G_GNUC_INTERNAL int pcmk__evaluate_rsc_expression(const xmlNode *expr, const pcmk_rule_input_t *rule_input); G_GNUC_INTERNAL int pcmk__evaluate_op_expression(const xmlNode *expr, const pcmk_rule_input_t *rule_input); /* * Utils */ #define PCMK__PW_BUFFER_LEN 500 /* * Schemas */ typedef struct { unsigned char v[2]; } pcmk__schema_version_t; enum pcmk__schema_validator { pcmk__schema_validator_none, pcmk__schema_validator_rng }; typedef struct { int schema_index; char *name; /*! * List of XSLT stylesheets for upgrading from this schema version to the * next one. Sorted by the order in which they should be applied to the CIB. */ GList *transforms; void *cache; enum pcmk__schema_validator validator; pcmk__schema_version_t version; } pcmk__schema_t; G_GNUC_INTERNAL GList *pcmk__find_x_0_schema(void); #ifdef __cplusplus } #endif #endif // PCMK__COMMON_CRMCOMMON_PRIVATE__H diff --git a/lib/common/nvpair.c b/lib/common/nvpair.c index 1116cdf330..d3a464be1f 100644 --- a/lib/common/nvpair.c +++ b/lib/common/nvpair.c @@ -1,994 +1,957 @@ /* * Copyright 2004-2024 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 "crmcommon_private.h" /* * This file isolates handling of various kinds of name/value pairs: * * - pcmk_nvpair_t data type * - XML attributes () * - XML nvpair elements () * - Meta-attributes (for resources and actions) */ // pcmk_nvpair_t handling /*! * \internal * \brief Allocate a new name/value pair * * \param[in] name New name (required) * \param[in] value New value * * \return Newly allocated name/value pair * \note The caller is responsible for freeing the result with * \c pcmk__free_nvpair(). */ static pcmk_nvpair_t * pcmk__new_nvpair(const char *name, const char *value) { pcmk_nvpair_t *nvpair = NULL; CRM_ASSERT(name); nvpair = pcmk__assert_alloc(1, sizeof(pcmk_nvpair_t)); nvpair->name = pcmk__str_copy(name); nvpair->value = pcmk__str_copy(value); return nvpair; } /*! * \internal * \brief Free a name/value pair * * \param[in,out] nvpair Name/value pair to free */ static void pcmk__free_nvpair(gpointer data) { if (data) { pcmk_nvpair_t *nvpair = data; free(nvpair->name); free(nvpair->value); free(nvpair); } } /*! * \brief Prepend a name/value pair to a list * * \param[in,out] nvpairs List to modify * \param[in] name New entry's name * \param[in] value New entry's value * * \return New head of list * \note The caller is responsible for freeing the list with * \c pcmk_free_nvpairs(). */ GSList * pcmk_prepend_nvpair(GSList *nvpairs, const char *name, const char *value) { return g_slist_prepend(nvpairs, pcmk__new_nvpair(name, value)); } /*! * \brief Free a list of name/value pairs * * \param[in,out] list List to free */ void pcmk_free_nvpairs(GSList *nvpairs) { g_slist_free_full(nvpairs, pcmk__free_nvpair); } -/*! - * \internal - * \brief Compare two name/value pairs - * - * \param[in] a First name/value pair to compare - * \param[in] b Second name/value pair to compare - * - * \return 0 if a == b, 1 if a > b, -1 if a < b - */ -static gint -pcmk__compare_nvpair(gconstpointer a, gconstpointer b) -{ - int rc = 0; - const pcmk_nvpair_t *pair_a = a; - const pcmk_nvpair_t *pair_b = b; - - CRM_ASSERT(a != NULL); - CRM_ASSERT(pair_a->name != NULL); - - CRM_ASSERT(b != NULL); - CRM_ASSERT(pair_b->name != NULL); - - rc = strcmp(pair_a->name, pair_b->name); - if (rc < 0) { - return -1; - } else if (rc > 0) { - return 1; - } - return 0; -} - -/*! - * \brief Sort a list of name/value pairs - * - * \param[in,out] list List to sort - * - * \return New head of list - */ -GSList * -pcmk_sort_nvpairs(GSList *list) -{ - return g_slist_sort(list, pcmk__compare_nvpair); -} - -/*! - * \brief Create a list of name/value pairs from an XML node's attributes - * - * \param[in] XML to parse - * - * \return New list of name/value pairs - * \note It is the caller's responsibility to free the list with - * \c pcmk_free_nvpairs(). - */ -GSList * -pcmk_xml_attrs2nvpairs(const xmlNode *xml) -{ - GSList *result = NULL; - - for (xmlAttrPtr iter = pcmk__xe_first_attr(xml); iter != NULL; - iter = iter->next) { - - result = pcmk_prepend_nvpair(result, - (const char *) iter->name, - (const char *) pcmk__xml_attr_value(iter)); - } - return result; -} - -/*! - * \internal - * \brief Add an XML attribute corresponding to a name/value pair - * - * Suitable for glib list iterators, this function adds a NAME=VALUE - * XML attribute based on a given name/value pair. - * - * \param[in] data Name/value pair - * \param[out] user_data XML node to add attributes to - */ -static void -pcmk__nvpair_add_xml_attr(gpointer data, gpointer user_data) -{ - pcmk_nvpair_t *pair = data; - xmlNode *parent = user_data; - - crm_xml_add(parent, pair->name, pair->value); -} - -/*! - * \brief Add XML attributes based on a list of name/value pairs - * - * \param[in,out] list List of name/value pairs - * \param[in,out] xml XML node to add attributes to - */ -void -pcmk_nvpairs2xml_attrs(GSList *list, xmlNode *xml) -{ - g_slist_foreach(list, pcmk__nvpair_add_xml_attr, xml); -} - // convenience function for name=value strings /*! * \internal * \brief Extract the name and value from an input string formatted as "name=value". * If unable to extract them, they are returned as NULL. * * \param[in] input The input string, likely from the command line * \param[out] name Everything before the first '=' in the input string * \param[out] value Everything after the first '=' in the input string * * \return 2 if both name and value could be extracted, 1 if only one could, and * and error code otherwise */ int pcmk__scan_nvpair(const char *input, char **name, char **value) { #ifdef HAVE_SSCANF_M *name = NULL; *value = NULL; if (sscanf(input, "%m[^=]=%m[^\n]", name, value) <= 0) { return -pcmk_err_bad_nvpair; } #else char *sep = NULL; *name = NULL; *value = NULL; sep = strstr(optarg, "="); if (sep == NULL) { return -pcmk_err_bad_nvpair; } *name = strndup(input, sep-input); if (*name == NULL) { return -ENOMEM; } /* If the last char in optarg is =, the user gave no * value for the option. Leave it as NULL. */ if (*(sep+1) != '\0') { *value = strdup(sep+1); if (*value == NULL) { return -ENOMEM; } } #endif if (*name != NULL && *value != NULL) { return 2; } else if (*name != NULL || *value != NULL) { return 1; } else { return -pcmk_err_bad_nvpair; } } /*! * \internal * \brief Format a name/value pair. * * Units can optionally be provided for the value. Note that unlike most * formatting functions, this one returns the formatted string. It is * assumed that the most common use of this function will be to build up * a string to be output as part of other functions. * * \note The caller is responsible for freeing the return value after use. * * \param[in] name The name of the nvpair. * \param[in] value The value of the nvpair. * \param[in] units Optional units for the value, or NULL. * * \return Newly allocated string with name/value pair */ char * pcmk__format_nvpair(const char *name, const char *value, const char *units) { return crm_strdup_printf("%s=\"%s%s\"", name, value, units ? units : ""); } // XML attribute handling /*! * \brief Create an XML attribute with specified name and value * * \param[in,out] node XML node to modify * \param[in] name Attribute name to set * \param[in] value Attribute value to set * * \return New value on success, \c NULL otherwise * \note This does nothing if node, name, or value are \c NULL or empty. */ const char * crm_xml_add(xmlNode *node, const char *name, const char *value) { + // @TODO Replace with internal function that returns the new attribute bool dirty = FALSE; xmlAttr *attr = NULL; CRM_CHECK(node != NULL, return NULL); CRM_CHECK(name != NULL, return NULL); if (value == NULL) { return NULL; } if (pcmk__tracking_xml_changes(node, FALSE)) { const char *old = crm_element_value(node, name); if (old == NULL || value == NULL || strcmp(old, value) != 0) { dirty = TRUE; } } if (dirty && (pcmk__check_acl(node, name, pcmk__xf_acl_create) == FALSE)) { crm_trace("Cannot add %s=%s to %s", name, value, node->name); return NULL; } attr = xmlSetProp(node, (pcmkXmlStr) name, (pcmkXmlStr) value); + + /* If the attribute already exists, this does nothing. Attribute values + * don't get private data. + */ + pcmk__xml_new_private_data((xmlNode *) attr); + if (dirty) { pcmk__mark_xml_attr_dirty(attr); } CRM_CHECK(attr && attr->children && attr->children->content, return NULL); return (char *)attr->children->content; } /*! * \brief Create an XML attribute with specified name and integer value * * This is like \c crm_xml_add() but taking an integer value. * * \param[in,out] node XML node to modify * \param[in] name Attribute name to set * \param[in] value Attribute value to set * * \return New value as string on success, \c NULL otherwise * \note This does nothing if node or name are \c NULL or empty. */ const char * crm_xml_add_int(xmlNode *node, const char *name, int value) { char *number = pcmk__itoa(value); const char *added = crm_xml_add(node, name, number); free(number); return added; } /*! * \brief Create an XML attribute with specified name and unsigned value * * This is like \c crm_xml_add() but taking a guint value. * * \param[in,out] node XML node to modify * \param[in] name Attribute name to set * \param[in] ms Attribute value to set * * \return New value as string on success, \c NULL otherwise * \note This does nothing if node or name are \c NULL or empty. */ const char * crm_xml_add_ms(xmlNode *node, const char *name, guint ms) { char *number = crm_strdup_printf("%u", ms); const char *added = crm_xml_add(node, name, number); free(number); return added; } // Maximum size of null-terminated string representation of 64-bit integer // -9223372036854775808 #define LLSTRSIZE 21 /*! * \brief Create an XML attribute with specified name and long long int value * * This is like \c crm_xml_add() but taking a long long int value. It is a * useful equivalent for defined types like time_t, etc. * * \param[in,out] xml XML node to modify * \param[in] name Attribute name to set * \param[in] value Attribute value to set * * \return New value as string on success, \c NULL otherwise * \note This does nothing if xml or name are \c NULL or empty. * This does not support greater than 64-bit values. */ const char * crm_xml_add_ll(xmlNode *xml, const char *name, long long value) { char s[LLSTRSIZE] = { '\0', }; if (snprintf(s, LLSTRSIZE, "%lld", (long long) value) == LLSTRSIZE) { return NULL; } return crm_xml_add(xml, name, s); } /*! * \brief Create XML attributes for seconds and microseconds * * This is like \c crm_xml_add() but taking a struct timeval. * * \param[in,out] xml XML node to modify * \param[in] name_sec Name of XML attribute for seconds * \param[in] name_usec Name of XML attribute for microseconds (or NULL) * \param[in] value Time value to set * * \return New seconds value as string on success, \c NULL otherwise * \note This does nothing if xml, name_sec, or value is \c NULL. */ const char * crm_xml_add_timeval(xmlNode *xml, const char *name_sec, const char *name_usec, const struct timeval *value) { const char *added = NULL; if (xml && name_sec && value) { added = crm_xml_add_ll(xml, name_sec, (long long) value->tv_sec); if (added && name_usec) { // Any error is ignored (we successfully added seconds) crm_xml_add_ll(xml, name_usec, (long long) value->tv_usec); } } return added; } /*! * \brief Retrieve the value of an XML attribute * * \param[in] data XML node to check * \param[in] name Attribute name to check * * \return Value of specified attribute (may be \c NULL) */ const char * crm_element_value(const xmlNode *data, const char *name) { xmlAttr *attr = NULL; if (data == NULL) { crm_err("Couldn't find %s in NULL", name ? name : ""); CRM_LOG_ASSERT(data != NULL); return NULL; } else if (name == NULL) { crm_err("Couldn't find NULL in %s", data->name); return NULL; } attr = xmlHasProp(data, (pcmkXmlStr) name); if (!attr || !attr->children) { return NULL; } return (const char *) attr->children->content; } /*! * \brief Retrieve the integer value of an XML attribute * * This is like \c crm_element_value() but getting the value as an integer. * * \param[in] data XML node to check * \param[in] name Attribute name to check * \param[out] dest Where to store element value * * \return 0 on success, -1 otherwise */ int crm_element_value_int(const xmlNode *data, const char *name, int *dest) { const char *value = NULL; CRM_CHECK(dest != NULL, return -1); value = crm_element_value(data, name); if (value) { long long value_ll; if ((pcmk__scan_ll(value, &value_ll, 0LL) != pcmk_rc_ok) || (value_ll < INT_MIN) || (value_ll > INT_MAX)) { *dest = PCMK__PARSE_INT_DEFAULT; } else { *dest = (int) value_ll; return 0; } } return -1; } /*! * \brief Retrieve the long long integer value of an XML attribute * * This is like \c crm_element_value() but getting the value as a long long int. * * \param[in] data XML node to check * \param[in] name Attribute name to check * \param[out] dest Where to store element value * * \return 0 on success, -1 otherwise */ int crm_element_value_ll(const xmlNode *data, const char *name, long long *dest) { const char *value = NULL; CRM_CHECK(dest != NULL, return -1); value = crm_element_value(data, name); if ((value != NULL) && (pcmk__scan_ll(value, dest, PCMK__PARSE_INT_DEFAULT) == pcmk_rc_ok)) { return 0; } return -1; } /*! * \brief Retrieve the millisecond value of an XML attribute * * This is like \c crm_element_value() but returning the value as a guint. * * \param[in] data XML node to check * \param[in] name Attribute name to check * \param[out] dest Where to store attribute value * * \return \c pcmk_ok on success, -1 otherwise */ int crm_element_value_ms(const xmlNode *data, const char *name, guint *dest) { const char *value = NULL; long long value_ll; CRM_CHECK(dest != NULL, return -1); *dest = 0; value = crm_element_value(data, name); if ((pcmk__scan_ll(value, &value_ll, 0LL) != pcmk_rc_ok) || (value_ll < 0) || (value_ll > G_MAXUINT)) { return -1; } *dest = (guint) value_ll; return pcmk_ok; } /*! * \brief Retrieve the seconds-since-epoch value of an XML attribute * * This is like \c crm_element_value() but returning the value as a time_t. * * \param[in] xml XML node to check * \param[in] name Attribute name to check * \param[out] dest Where to store attribute value * * \return \c pcmk_ok on success, -1 otherwise */ int crm_element_value_epoch(const xmlNode *xml, const char *name, time_t *dest) { long long value_ll = 0; if (crm_element_value_ll(xml, name, &value_ll) < 0) { return -1; } /* Unfortunately, we can't do any bounds checking, since time_t has neither * standardized bounds nor constants defined for them. */ *dest = (time_t) value_ll; return pcmk_ok; } /*! * \brief Retrieve the value of XML second/microsecond attributes as time * * This is like \c crm_element_value() but returning value as a struct timeval. * * \param[in] xml XML to parse * \param[in] name_sec Name of XML attribute for seconds * \param[in] name_usec Name of XML attribute for microseconds * \param[out] dest Where to store result * * \return \c pcmk_ok on success, -errno on error * \note Values default to 0 if XML or XML attribute does not exist */ int crm_element_value_timeval(const xmlNode *xml, const char *name_sec, const char *name_usec, struct timeval *dest) { long long value_i = 0; CRM_CHECK(dest != NULL, return -EINVAL); dest->tv_sec = 0; dest->tv_usec = 0; if (xml == NULL) { return pcmk_ok; } /* Unfortunately, we can't do any bounds checking, since there are no * constants provided for the bounds of time_t and suseconds_t, and * calculating them isn't worth the effort. If there are XML values * beyond the native sizes, there will probably be worse problems anyway. */ // Parse seconds errno = 0; if (crm_element_value_ll(xml, name_sec, &value_i) < 0) { return -errno; } dest->tv_sec = (time_t) value_i; // Parse microseconds if (crm_element_value_ll(xml, name_usec, &value_i) < 0) { return -errno; } dest->tv_usec = (suseconds_t) value_i; return pcmk_ok; } /*! * \internal * \brief Get a date/time object from an XML attribute value * * \param[in] xml XML with attribute to parse (from CIB) * \param[in] attr Name of attribute to parse * \param[out] t Where to create date/time object * (\p *t must be NULL initially) * * \return Standard Pacemaker return code * \note The caller is responsible for freeing \p *t using crm_time_free(). */ int pcmk__xe_get_datetime(const xmlNode *xml, const char *attr, crm_time_t **t) { const char *value = NULL; if ((t == NULL) || (*t != NULL) || (xml == NULL) || (attr == NULL)) { return EINVAL; } value = crm_element_value(xml, attr); if (value != NULL) { *t = crm_time_new(value); if (*t == NULL) { return pcmk_rc_unpack_error; } } return pcmk_rc_ok; } /*! * \brief Retrieve a copy of the value of an XML attribute * * This is like \c crm_element_value() but allocating new memory for the result. * * \param[in] data XML node to check * \param[in] name Attribute name to check * * \return Value of specified attribute (may be \c NULL) * \note The caller is responsible for freeing the result. */ char * crm_element_value_copy(const xmlNode *data, const char *name) { return pcmk__str_copy(crm_element_value(data, name)); } /*! * \brief Safely add hash table entry to XML as attribute or name-value pair * * Suitable for \c g_hash_table_foreach(), this function takes a hash table key * and value, with an XML node passed as user data, and adds an XML attribute * with the specified name and value if it does not already exist. If the key * name starts with a digit, then it's not a valid XML attribute name. In that * case, this will instead add a child * to the XML. * * \param[in] key Key of hash table entry * \param[in] value Value of hash table entry * \param[in,out] user_data XML node */ void hash2smartfield(gpointer key, gpointer value, gpointer user_data) { /* @TODO Generate PCMK__XE_PARAM nodes for all keys that aren't valid XML * attribute names (not just those that start with digits), or possibly for * all keys to simplify parsing. * * Consider either deprecating as public API or exposing PCMK__XE_PARAM. * PCMK__XE_PARAM is currently private because it doesn't appear in any * output that Pacemaker generates. */ const char *name = key; const char *s_value = value; xmlNode *xml_node = user_data; if (isdigit(name[0])) { xmlNode *tmp = pcmk__xe_create(xml_node, PCMK__XE_PARAM); crm_xml_add(tmp, PCMK_XA_NAME, name); crm_xml_add(tmp, PCMK_XA_VALUE, s_value); } else if (crm_element_value(xml_node, name) == NULL) { crm_xml_add(xml_node, name, s_value); crm_trace("dumped: %s=%s", name, s_value); } else { crm_trace("duplicate: %s=%s", name, s_value); } } /*! * \brief Set XML attribute based on hash table entry * * Suitable for \c g_hash_table_foreach(), this function takes a hash table key * and value, with an XML node passed as user data, and adds an XML attribute * with the specified name and value if it does not already exist. * * \param[in] key Key of hash table entry * \param[in] value Value of hash table entry * \param[in,out] user_data XML node */ void hash2field(gpointer key, gpointer value, gpointer user_data) { const char *name = key; const char *s_value = value; xmlNode *xml_node = user_data; if (crm_element_value(xml_node, name) == NULL) { crm_xml_add(xml_node, name, s_value); } else { crm_trace("duplicate: %s=%s", name, s_value); } } /*! * \brief Set XML attribute based on hash table entry, as meta-attribute name * * Suitable for \c g_hash_table_foreach(), this function takes a hash table key * and value, with an XML node passed as user data, and adds an XML attribute * with the meta-attribute version of the specified name and value if it does * not already exist and if the name does not appear to be cluster-internal. * * \param[in] key Key of hash table entry * \param[in] value Value of hash table entry * \param[in,out] user_data XML node */ void hash2metafield(gpointer key, gpointer value, gpointer user_data) { char *crm_name = NULL; if (key == NULL || value == NULL) { return; } /* Filter out cluster-generated attributes that contain a '#' or ':' * (like fail-count and last-failure). */ for (crm_name = key; *crm_name; ++crm_name) { if ((*crm_name == '#') || (*crm_name == ':')) { return; } } crm_name = crm_meta_name(key); hash2field(crm_name, value, user_data); free(crm_name); } // nvpair handling /*! * \brief Create an XML name/value pair * * \param[in,out] parent If not \c NULL, make new XML node a child of this one * \param[in] id Set this as XML ID (or NULL to auto-generate) * \param[in] name Name to use * \param[in] value Value to use * * \return New XML object on success, \c NULL otherwise */ xmlNode * crm_create_nvpair_xml(xmlNode *parent, const char *id, const char *name, const char *value) { xmlNode *nvp; /* id can be NULL so we auto-generate one, and name can be NULL if this * will be used to delete a name/value pair by ID, but both can't be NULL */ CRM_CHECK(id || name, return NULL); nvp = pcmk__xe_create(parent, PCMK_XE_NVPAIR); if (id) { crm_xml_add(nvp, PCMK_XA_ID, id); } else { pcmk__xe_set_id(nvp, "%s-%s", pcmk__s(pcmk__xe_id(parent), PCMK_XE_NVPAIR), name); } crm_xml_add(nvp, PCMK_XA_NAME, name); crm_xml_add(nvp, PCMK_XA_VALUE, value); return nvp; } -/*! - * \brief Add XML nvpair element based on hash table entry - * - * Suitable for \c g_hash_table_foreach(), this function takes a hash table key - * and value, with an XML node passed as the user data, and adds an \c nvpair - * XML element with the specified name and value. - * - * \param[in] key Key of hash table entry - * \param[in] value Value of hash table entry - * \param[in,out] user_data XML node - */ -void -hash2nvpair(gpointer key, gpointer value, gpointer user_data) -{ - const char *name = key; - const char *s_value = value; - xmlNode *xml_node = user_data; - - crm_create_nvpair_xml(xml_node, name, name, s_value); - crm_trace("dumped: name=%s value=%s", name, s_value); -} - /*! * \brief Retrieve XML attributes as a hash table * * Given an XML element, this will look for any \ element child, * creating a hash table of (newly allocated string) name/value pairs taken * first from the attributes element's NAME=VALUE XML attributes, and then * from any \ children of attributes. * * \param[in] XML node to parse * * \return Hash table with name/value pairs * \note It is the caller's responsibility to free the result using * \c g_hash_table_destroy(). */ GHashTable * xml2list(const xmlNode *parent) { xmlNode *child = NULL; xmlAttrPtr pIter = NULL; xmlNode *nvpair_list = NULL; GHashTable *nvpair_hash = pcmk__strkey_table(free, free); CRM_CHECK(parent != NULL, return nvpair_hash); nvpair_list = pcmk__xe_first_child(parent, PCMK__XE_ATTRIBUTES, NULL, NULL); if (nvpair_list == NULL) { crm_trace("No attributes in %s", parent->name); crm_log_xml_trace(parent, "No attributes for resource op"); } crm_log_xml_trace(nvpair_list, "Unpacking"); for (pIter = pcmk__xe_first_attr(nvpair_list); pIter != NULL; pIter = pIter->next) { const char *p_name = (const char *)pIter->name; const char *p_value = pcmk__xml_attr_value(pIter); crm_trace("Added %s=%s", p_name, p_value); pcmk__insert_dup(nvpair_hash, p_name, p_value); } for (child = pcmk__xe_first_child(nvpair_list, PCMK__XE_PARAM, NULL, NULL); child != NULL; child = pcmk__xe_next_same(child)) { const char *key = crm_element_value(child, PCMK_XA_NAME); const char *value = crm_element_value(child, PCMK_XA_VALUE); crm_trace("Added %s=%s", key, value); if (key != NULL && value != NULL) { pcmk__insert_dup(nvpair_hash, key, value); } } return nvpair_hash; } void pcmk__xe_set_bool_attr(xmlNodePtr node, const char *name, bool value) { crm_xml_add(node, name, pcmk__btoa(value)); } int pcmk__xe_get_bool_attr(const xmlNode *node, const char *name, bool *value) { const char *xml_value = NULL; int ret, rc; if (node == NULL) { return ENODATA; } else if (name == NULL || value == NULL) { return EINVAL; } xml_value = crm_element_value(node, name); if (xml_value == NULL) { return ENODATA; } rc = crm_str_to_boolean(xml_value, &ret); if (rc == 1) { *value = ret; return pcmk_rc_ok; } else { return pcmk_rc_bad_input; } } bool pcmk__xe_attr_is_true(const xmlNode *node, const char *name) { bool value = false; int rc; rc = pcmk__xe_get_bool_attr(node, name, &value); return rc == pcmk_rc_ok && value == true; } // Meta-attribute handling /*! * \brief Get the environment variable equivalent of a meta-attribute name * * \param[in] attr_name Name of meta-attribute * * \return Newly allocated string for \p attr_name with "CRM_meta_" prefix and * underbars instead of dashes * \note This asserts on an invalid argument or memory allocation error, so * callers can assume the result is non-NULL. The caller is responsible * for freeing the result using free(). */ char * crm_meta_name(const char *attr_name) { char *env_name = NULL; CRM_ASSERT(!pcmk__str_empty(attr_name)); env_name = crm_strdup_printf(CRM_META "_%s", attr_name); for (char *c = env_name; *c != '\0'; ++c) { if (*c == '-') { *c = '_'; } } return env_name; } /*! * \brief Get the value of a meta-attribute * * Get the value of a meta-attribute from a hash table whose keys are * meta-attribute environment variable names (as crm_meta_name() would * create, like pcmk__graph_action_t:params, not pcmk_resource_t:meta). * * \param[in] meta Hash table of meta-attributes * \param[in] attr_name Name of meta-attribute to get * * \return Value of given meta-attribute */ const char * crm_meta_value(GHashTable *meta, const char *attr_name) { if ((meta != NULL) && (attr_name != NULL)) { char *key = crm_meta_name(attr_name); const char *value = g_hash_table_lookup(meta, key); free(key); return value; } return NULL; } + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +static gint +pcmk__compare_nvpair(gconstpointer a, gconstpointer b) +{ + int rc = 0; + const pcmk_nvpair_t *pair_a = a; + const pcmk_nvpair_t *pair_b = b; + + CRM_ASSERT(a != NULL); + CRM_ASSERT(pair_a->name != NULL); + + CRM_ASSERT(b != NULL); + CRM_ASSERT(pair_b->name != NULL); + + rc = strcmp(pair_a->name, pair_b->name); + if (rc < 0) { + return -1; + } else if (rc > 0) { + return 1; + } + return 0; +} + +GSList * +pcmk_sort_nvpairs(GSList *list) +{ + return g_slist_sort(list, pcmk__compare_nvpair); +} + +GSList * +pcmk_xml_attrs2nvpairs(const xmlNode *xml) +{ + GSList *result = NULL; + + for (xmlAttrPtr iter = pcmk__xe_first_attr(xml); iter != NULL; + iter = iter->next) { + + result = pcmk_prepend_nvpair(result, + (const char *) iter->name, + (const char *) pcmk__xml_attr_value(iter)); + } + return result; +} + +static void +pcmk__nvpair_add_xml_attr(gpointer data, gpointer user_data) +{ + pcmk_nvpair_t *pair = data; + xmlNode *parent = user_data; + + crm_xml_add(parent, pair->name, pair->value); +} + +void +pcmk_nvpairs2xml_attrs(GSList *list, xmlNode *xml) +{ + g_slist_foreach(list, pcmk__nvpair_add_xml_attr, xml); +} + +void +hash2nvpair(gpointer key, gpointer value, gpointer user_data) +{ + const char *name = key; + const char *s_value = value; + xmlNode *xml_node = user_data; + + crm_create_nvpair_xml(xml_node, name, name, s_value); + crm_trace("dumped: name=%s value=%s", name, s_value); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/output.c b/lib/common/output.c index 92fbfdac41..f954425b79 100644 --- a/lib/common/output.c +++ b/lib/common/output.c @@ -1,337 +1,337 @@ /* * Copyright 2019-2024 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 "crmcommon_private.h" static GHashTable *formatters = NULL; #if defined(PCMK__UNIT_TESTING) // LCOV_EXCL_START GHashTable * pcmk__output_formatters(void) { return formatters; } // LCOV_EXCL_STOP #endif void pcmk__output_free(pcmk__output_t *out) { if (out == NULL) { return; } out->free_priv(out); if (out->messages != NULL) { g_hash_table_destroy(out->messages); } g_free(out->request); free(out); } /*! * \internal * \brief Create a new \p pcmk__output_t structure * * This function does not register any message functions with the newly created * object. * * \param[in,out] out Where to store the new output object * \param[in] fmt_name How to format output * \param[in] filename Where to write formatted output. This can be a * filename (the file will be overwritten if it already * exists), or \p NULL or \p "-" for stdout. For no * output, pass a filename of \p "/dev/null". * \param[in] argv List of command line arguments * * \return Standard Pacemaker return code */ int pcmk__bare_output_new(pcmk__output_t **out, const char *fmt_name, const char *filename, char **argv) { pcmk__output_factory_t create = NULL; CRM_ASSERT(formatters != NULL && out != NULL); /* If no name was given, just try "text". It's up to each tool to register * what it supports so this also may not be valid. */ if (fmt_name == NULL) { create = g_hash_table_lookup(formatters, "text"); } else { create = g_hash_table_lookup(formatters, fmt_name); } if (create == NULL) { return pcmk_rc_unknown_format; } *out = create(argv); if (*out == NULL) { return ENOMEM; } if (pcmk__str_eq(filename, "-", pcmk__str_null_matches)) { (*out)->dest = stdout; } else { (*out)->dest = fopen(filename, "w"); if ((*out)->dest == NULL) { pcmk__output_free(*out); *out = NULL; return errno; } } (*out)->quiet = false; (*out)->messages = pcmk__strkey_table(free, NULL); if ((*out)->init(*out) == false) { pcmk__output_free(*out); return ENOMEM; } setenv("OCF_OUTPUT_FORMAT", (*out)->fmt_name, 1); return pcmk_rc_ok; } int pcmk__output_new(pcmk__output_t **out, const char *fmt_name, const char *filename, char **argv) { int rc = pcmk__bare_output_new(out, fmt_name, filename, argv); if (rc == pcmk_rc_ok) { // Register libcrmcommon messages pcmk__register_option_messages(*out); pcmk__register_patchset_messages(*out); } return rc; } int pcmk__register_format(GOptionGroup *group, const char *name, pcmk__output_factory_t create, const GOptionEntry *options) { char *name_copy = NULL; CRM_ASSERT(create != NULL && !pcmk__str_empty(name)); name_copy = strdup(name); if (name_copy == NULL) { return ENOMEM; } if (formatters == NULL) { formatters = pcmk__strkey_table(free, NULL); } if (options != NULL && group != NULL) { g_option_group_add_entries(group, options); } g_hash_table_insert(formatters, name_copy, create); return pcmk_rc_ok; } void pcmk__register_formats(GOptionGroup *group, const pcmk__supported_format_t *formats) { if (formats == NULL) { return; } for (const pcmk__supported_format_t *entry = formats; entry->name != NULL; entry++) { pcmk__register_format(group, entry->name, entry->create, entry->options); } } void pcmk__unregister_formats(void) { if (formatters != NULL) { g_hash_table_destroy(formatters); formatters = NULL; } } int pcmk__call_message(pcmk__output_t *out, const char *message_id, ...) { va_list args; int rc = pcmk_rc_ok; pcmk__message_fn_t fn; CRM_ASSERT(out != NULL && !pcmk__str_empty(message_id)); fn = g_hash_table_lookup(out->messages, message_id); if (fn == NULL) { crm_debug("Called unknown output message '%s' for format '%s'", message_id, out->fmt_name); return EINVAL; } va_start(args, message_id); rc = fn(out, args); va_end(args); return rc; } void pcmk__register_message(pcmk__output_t *out, const char *message_id, pcmk__message_fn_t fn) { CRM_ASSERT(out != NULL && !pcmk__str_empty(message_id) && fn != NULL); g_hash_table_replace(out->messages, pcmk__str_copy(message_id), fn); } void pcmk__register_messages(pcmk__output_t *out, const pcmk__message_entry_t *table) { for (const pcmk__message_entry_t *entry = table; entry->message_id != NULL; entry++) { if (pcmk__strcase_any_of(entry->fmt_name, "default", out->fmt_name, NULL)) { pcmk__register_message(out, entry->message_id, entry->fn); } } } void pcmk__output_and_clear_error(GError **error, pcmk__output_t *out) { if (error == NULL || *error == NULL) { return; } if (out != NULL) { out->err(out, "%s: %s", g_get_prgname(), (*error)->message); } else { fprintf(stderr, "%s: %s\n", g_get_prgname(), (*error)->message); } g_clear_error(error); } /*! * \internal * \brief Create an XML-only output object * * Create an output object that supports only the XML format, and free * existing XML if supplied (particularly useful for libpacemaker public API * functions that want to free any previous result supplied by the caller). * * \param[out] out Where to put newly created output object * \param[in,out] xml If \c *xml is non-NULL, this will be freed * * \return Standard Pacemaker return code */ int pcmk__xml_output_new(pcmk__output_t **out, xmlNodePtr *xml) { pcmk__supported_format_t xml_format[] = { PCMK__SUPPORTED_FORMAT_XML, { NULL, NULL, NULL } }; if (xml == NULL) { return EINVAL; } if (*xml != NULL) { - xmlFreeNode(*xml); + pcmk__xml_free(*xml); *xml = NULL; } pcmk__register_formats(NULL, xml_format); return pcmk__output_new(out, "xml", NULL, NULL); } /*! * \internal * \brief Finish and free an XML-only output object * * \param[in,out] out Output object to free * \param[in] exit_status The exit value of the whole program * \param[out] xml If not NULL, where to store XML output */ void pcmk__xml_output_finish(pcmk__output_t *out, crm_exit_t exit_status, xmlNodePtr *xml) { if (out == NULL) { return; } out->finish(out, exit_status, FALSE, (void **) xml); pcmk__output_free(out); } /*! * \internal * \brief Create a new output object using the "log" format * * \param[out] out Where to store newly allocated output object * * \return Standard Pacemaker return code */ int pcmk__log_output_new(pcmk__output_t **out) { int rc = pcmk_rc_ok; const char* argv[] = { "", NULL }; pcmk__supported_format_t formats[] = { PCMK__SUPPORTED_FORMAT_LOG, { NULL, NULL, NULL } }; pcmk__register_formats(NULL, formats); rc = pcmk__output_new(out, "log", NULL, (char **) argv); if ((rc != pcmk_rc_ok) || (*out == NULL)) { crm_err("Can't log certain messages due to internal error: %s", pcmk_rc_str(rc)); return rc; } return pcmk_rc_ok; } /*! * \internal * \brief Create a new output object using the "text" format * * \param[out] out Where to store newly allocated output object * \param[in] filename Name of output destination file * * \return Standard Pacemaker return code */ int pcmk__text_output_new(pcmk__output_t **out, const char *filename) { int rc = pcmk_rc_ok; const char* argv[] = { "", NULL }; pcmk__supported_format_t formats[] = { PCMK__SUPPORTED_FORMAT_TEXT, { NULL, NULL, NULL } }; pcmk__register_formats(NULL, formats); rc = pcmk__output_new(out, "text", filename, (char **) argv); if ((rc != pcmk_rc_ok) || (*out == NULL)) { crm_err("Can't create text output object to internal error: %s", pcmk_rc_str(rc)); return rc; } return pcmk_rc_ok; } diff --git a/lib/common/output_html.c b/lib/common/output_html.c index 4611665624..f50e3e74b1 100644 --- a/lib/common/output_html.c +++ b/lib/common/output_html.c @@ -1,519 +1,518 @@ /* * Copyright 2019-2024 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 static const char *stylesheet_default = "." PCMK__VALUE_BOLD " { font-weight: bold }\n" "." PCMK_VALUE_ONLINE " { color: green }\n" "." PCMK_VALUE_OFFLINE " { color: red }\n" "." PCMK__VALUE_MAINT " { color: blue }\n" "." PCMK_VALUE_STANDBY " { color: blue }\n" "." PCMK__VALUE_HEALTH_RED " { color: red }\n" "." PCMK__VALUE_HEALTH_YELLOW " { color: GoldenRod }\n" "." PCMK__VALUE_RSC_FAILED " { color: red }\n" "." PCMK__VALUE_RSC_FAILURE_IGNORED " { color: DarkGreen }\n" "." PCMK__VALUE_RSC_MANAGED " { color: blue }\n" "." PCMK__VALUE_RSC_MULTIPLE " { color: orange }\n" "." PCMK__VALUE_RSC_OK " { color: green }\n" "." PCMK__VALUE_WARNING " { color: red; font-weight: bold }"; static gboolean cgi_output = FALSE; static char *stylesheet_link = NULL; static char *title = NULL; static GSList *extra_headers = NULL; GOptionEntry pcmk__html_output_entries[] = { { "html-cgi", 0, 0, G_OPTION_ARG_NONE, &cgi_output, "Add CGI headers (requires --output-as=html)", NULL }, { "html-stylesheet", 0, 0, G_OPTION_ARG_STRING, &stylesheet_link, "Link to an external stylesheet (requires --output-as=html)", "URI" }, { "html-title", 0, 0, G_OPTION_ARG_STRING, &title, "Specify a page title (requires --output-as=html)", "TITLE" }, { NULL } }; /* The first several elements of this struct must be the same as the first * several elements of private_data_s in lib/common/output_xml.c. This * struct gets passed to a bunch of the pcmk__output_xml_* functions which * assume an XML private_data_s. Keeping them laid out the same means this * still works. */ typedef struct private_data_s { /* Begin members that must match the XML version */ xmlNode *root; GQueue *parent_q; GSList *errors; /* End members that must match the XML version */ } private_data_t; static void html_free_priv(pcmk__output_t *out) { private_data_t *priv = NULL; if (out == NULL || out->priv == NULL) { return; } priv = out->priv; pcmk__xml_free(priv->root); /* The elements of parent_q are xmlNodes that are a part of the * priv->root document, so the above line already frees them. Don't * call g_queue_free_full here. */ g_queue_free(priv->parent_q); g_slist_free_full(priv->errors, free); free(priv); out->priv = NULL; } static bool html_init(pcmk__output_t *out) { private_data_t *priv = NULL; CRM_ASSERT(out != NULL); /* If html_init was previously called on this output struct, just return. */ if (out->priv != NULL) { return true; } else { out->priv = calloc(1, sizeof(private_data_t)); if (out->priv == NULL) { return false; } priv = out->priv; } priv->parent_q = g_queue_new(); priv->root = pcmk__xe_create(NULL, "html"); xmlCreateIntSubset(priv->root->doc, (pcmkXmlStr) "html", NULL, NULL); crm_xml_add(priv->root, PCMK_XA_LANG, PCMK__VALUE_EN); g_queue_push_tail(priv->parent_q, priv->root); priv->errors = NULL; pcmk__output_xml_create_parent(out, "body", NULL); return true; } static void add_error_node(gpointer data, gpointer user_data) { char *str = (char *) data; pcmk__output_t *out = (pcmk__output_t *) user_data; out->list_item(out, NULL, "%s", str); } static void html_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) { private_data_t *priv = NULL; htmlNodePtr head_node = NULL; htmlNodePtr charset_node = NULL; xmlNode *child_node = NULL; CRM_ASSERT(out != NULL); priv = out->priv; /* If root is NULL, html_init failed and we are being called from pcmk__output_free * in the pcmk__output_new path. */ if (priv == NULL || priv->root == NULL) { return; } if (cgi_output && print) { fprintf(out->dest, "Content-Type: text/html\n\n"); } /* Add the head node last - it's not needed earlier because it doesn't contain * anything else that the user could add, and we want it done last to pick up * any options that may have been given. */ - head_node = xmlNewDocRawNode(NULL, NULL, (pcmkXmlStr) "head", NULL); + head_node = pcmk__xe_create(priv->root, "head"); + xmlAddPrevSibling(priv->root->children, head_node); if (title != NULL ) { child_node = pcmk__xe_create(head_node, "title"); pcmk__xe_set_content(child_node, "%s", title); } else if (out->request != NULL) { child_node = pcmk__xe_create(head_node, "title"); pcmk__xe_set_content(child_node, "%s", out->request); } charset_node = pcmk__xe_create(head_node, PCMK__XE_META); crm_xml_add(charset_node, "charset", "utf-8"); /* Add any extra header nodes the caller might have created. */ - for (int i = 0; i < g_slist_length(extra_headers); i++) { - xmlAddChild(head_node, xmlCopyNode(g_slist_nth_data(extra_headers, i), 1)); + for (GSList *iter = extra_headers; iter != NULL; iter = iter->next) { + pcmk__xml_copy(head_node, (xmlNode *) iter->data); } /* Stylesheets are included two different ways. The first is via a built-in * default (see the stylesheet_default const above). The second is via the * html-stylesheet option, and this should obviously be a link to a * stylesheet. The second can override the first. At least one should be * given. */ child_node = pcmk__xe_create(head_node, "style"); pcmk__xe_set_content(child_node, "%s", stylesheet_default); if (stylesheet_link != NULL) { htmlNodePtr link_node = pcmk__xe_create(head_node, "link"); pcmk__xe_set_props(link_node, "rel", "stylesheet", "href", stylesheet_link, NULL); } - xmlAddPrevSibling(priv->root->children, head_node); - if (g_slist_length(priv->errors) > 0) { out->begin_list(out, "Errors", NULL, NULL); g_slist_foreach(priv->errors, add_error_node, (gpointer) out); out->end_list(out); } if (print) { htmlDocDump(out->dest, priv->root->doc); } if (copy_dest != NULL) { *copy_dest = pcmk__xml_copy(NULL, priv->root); } - g_slist_free_full(extra_headers, (GDestroyNotify) xmlFreeNode); + g_slist_free_full(extra_headers, (GDestroyNotify) pcmk__xml_free); extra_headers = NULL; } static void html_reset(pcmk__output_t *out) { CRM_ASSERT(out != NULL); out->dest = freopen(NULL, "w", out->dest); CRM_ASSERT(out->dest != NULL); html_free_priv(out); html_init(out); } static void html_subprocess_output(pcmk__output_t *out, int exit_status, const char *proc_stdout, const char *proc_stderr) { char *rc_buf = NULL; CRM_ASSERT(out != NULL); rc_buf = crm_strdup_printf("Return code: %d", exit_status); pcmk__output_create_xml_text_node(out, "h2", "Command Output"); pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, rc_buf); if (proc_stdout != NULL) { pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, "Stdout"); pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, PCMK__VALUE_OUTPUT, proc_stdout); } if (proc_stderr != NULL) { pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, "Stderr"); pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, PCMK__VALUE_OUTPUT, proc_stderr); } free(rc_buf); } static void html_version(pcmk__output_t *out, bool extended) { CRM_ASSERT(out != NULL); pcmk__output_create_xml_text_node(out, "h2", "Version Information"); pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, "Program: Pacemaker"); pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, "Version: " PACEMAKER_VERSION); pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, "Author: Andrew Beekhof and " "the Pacemaker project contributors"); pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, "Build: " BUILD_VERSION); pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, "Features: " CRM_FEATURES); } G_GNUC_PRINTF(2, 3) static void html_err(pcmk__output_t *out, const char *format, ...) { private_data_t *priv = NULL; int len = 0; char *buf = NULL; va_list ap; CRM_ASSERT(out != NULL && out->priv != NULL); priv = out->priv; va_start(ap, format); len = vasprintf(&buf, format, ap); CRM_ASSERT(len >= 0); va_end(ap); priv->errors = g_slist_append(priv->errors, buf); } G_GNUC_PRINTF(2, 3) static int html_info(pcmk__output_t *out, const char *format, ...) { return pcmk_rc_no_output; } static void html_output_xml(pcmk__output_t *out, const char *name, const char *buf) { htmlNodePtr node = NULL; CRM_ASSERT(out != NULL); node = pcmk__output_create_html_node(out, "pre", NULL, NULL, buf); crm_xml_add(node, PCMK_XA_LANG, "xml"); } G_GNUC_PRINTF(4, 5) static void html_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plural_noun, const char *format, ...) { int q_len = 0; private_data_t *priv = NULL; xmlNodePtr node = NULL; CRM_ASSERT(out != NULL && out->priv != NULL); priv = out->priv; /* If we are already in a list (the queue depth is always at least * one because of the element), first create a
  • element * to hold the

    and the new list. */ q_len = g_queue_get_length(priv->parent_q); if (q_len > 2) { pcmk__output_xml_create_parent(out, "li", NULL); } if (format != NULL) { va_list ap; char *buf = NULL; int len; va_start(ap, format); len = vasprintf(&buf, format, ap); va_end(ap); CRM_ASSERT(len >= 0); if (q_len > 2) { pcmk__output_create_xml_text_node(out, "h3", buf); } else { pcmk__output_create_xml_text_node(out, "h2", buf); } free(buf); } node = pcmk__output_xml_create_parent(out, "ul", NULL); g_queue_push_tail(priv->parent_q, node); } G_GNUC_PRINTF(3, 4) static void html_list_item(pcmk__output_t *out, const char *name, const char *format, ...) { htmlNodePtr item_node = NULL; va_list ap; char *buf = NULL; int len; CRM_ASSERT(out != NULL); va_start(ap, format); len = vasprintf(&buf, format, ap); CRM_ASSERT(len >= 0); va_end(ap); item_node = pcmk__output_create_xml_text_node(out, "li", buf); free(buf); if (name != NULL) { crm_xml_add(item_node, PCMK_XA_CLASS, name); } } static void html_increment_list(pcmk__output_t *out) { /* This function intentially left blank */ } static void html_end_list(pcmk__output_t *out) { private_data_t *priv = NULL; CRM_ASSERT(out != NULL && out->priv != NULL); priv = out->priv; /* Remove the
      tag, but do not free this result - it's still * part of the document. */ g_queue_pop_tail(priv->parent_q); pcmk__output_xml_pop_parent(out); /* Remove the
    • created for nested lists. */ if (g_queue_get_length(priv->parent_q) > 2) { pcmk__output_xml_pop_parent(out); } } static bool html_is_quiet(pcmk__output_t *out) { return false; } static void html_spacer(pcmk__output_t *out) { CRM_ASSERT(out != NULL); pcmk__output_create_xml_node(out, "br", NULL); } static void html_progress(pcmk__output_t *out, bool end) { /* This function intentially left blank */ } pcmk__output_t * pcmk__mk_html_output(char **argv) { pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); if (retval == NULL) { return NULL; } retval->fmt_name = "html"; retval->request = pcmk__quote_cmdline(argv); retval->init = html_init; retval->free_priv = html_free_priv; retval->finish = html_finish; retval->reset = html_reset; retval->register_message = pcmk__register_message; retval->message = pcmk__call_message; retval->subprocess_output = html_subprocess_output; retval->version = html_version; retval->info = html_info; retval->transient = html_info; retval->err = html_err; retval->output_xml = html_output_xml; retval->begin_list = html_begin_list; retval->list_item = html_list_item; retval->increment_list = html_increment_list; retval->end_list = html_end_list; retval->is_quiet = html_is_quiet; retval->spacer = html_spacer; retval->progress = html_progress; retval->prompt = pcmk__text_prompt; return retval; } xmlNodePtr pcmk__output_create_html_node(pcmk__output_t *out, const char *element_name, const char *id, const char *class_name, const char *text) { htmlNodePtr node = NULL; CRM_ASSERT(out != NULL); CRM_CHECK(pcmk__str_eq(out->fmt_name, "html", pcmk__str_none), return NULL); node = pcmk__output_create_xml_text_node(out, element_name, text); if (class_name != NULL) { crm_xml_add(node, PCMK_XA_CLASS, class_name); } if (id != NULL) { crm_xml_add(node, PCMK_XA_ID, id); } return node; } /*! * \internal * \brief Create a new HTML element under a given parent with ID and class * * \param[in,out] parent XML element that will be the new element's parent * (\c NULL to create a new XML document with the new * node as root) * \param[in] name Name of new element * \param[in] id CSS ID of new element (can be \c NULL) * \param[in] class_name CSS class of new element (can be \c NULL) * * \return Newly created XML element (guaranteed not to be \c NULL) */ xmlNode * pcmk__html_create(xmlNode *parent, const char *name, const char *id, const char *class_name) { xmlNode *node = pcmk__xe_create(parent, name); pcmk__xe_set_props(node, PCMK_XA_CLASS, class_name, PCMK_XA_ID, id, NULL); return node; } void pcmk__html_add_header(const char *name, ...) { htmlNodePtr header_node; va_list ap; va_start(ap, name); - header_node = xmlNewDocRawNode(NULL, NULL, (pcmkXmlStr) name, NULL); + header_node = pcmk__xe_create(NULL, name); while (1) { char *key = va_arg(ap, char *); char *value; if (key == NULL) { break; } value = va_arg(ap, char *); crm_xml_add(header_node, key, value); } extra_headers = g_slist_append(extra_headers, header_node); va_end(ap); } diff --git a/lib/common/patchset.c b/lib/common/patchset.c index 19ef506f07..e4781c8876 100644 --- a/lib/common/patchset.c +++ b/lib/common/patchset.c @@ -1,890 +1,879 @@ /* * Copyright 2004-2024 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 // CRM_XML_LOG_BASE, etc. #include "crmcommon_private.h" /* Add changes for specified XML to patchset. * For patchset format, refer to diff schema. */ static void add_xml_changes_to_patchset(xmlNode *xml, xmlNode *patchset) { xmlNode *cIter = NULL; xmlAttr *pIter = NULL; xmlNode *change = NULL; xml_node_private_t *nodepriv = xml->_private; const char *value = NULL; if (nodepriv == NULL) { /* Elements that shouldn't occur in a CIB don't have _private set. They * should be stripped out, ignored, or have an error thrown by any code * that processes their parent, so we ignore any changes to them. */ return; } // If this XML node is new, just report that if (patchset && pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { GString *xpath = pcmk__element_xpath(xml->parent); if (xpath != NULL) { int position = pcmk__xml_position(xml, pcmk__xf_deleted); change = pcmk__xe_create(patchset, PCMK_XE_CHANGE); crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_CREATE); crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str); crm_xml_add_int(change, PCMK_XE_POSITION, position); pcmk__xml_copy(change, xml); g_string_free(xpath, TRUE); } return; } // Check each of the XML node's attributes for changes for (pIter = pcmk__xe_first_attr(xml); pIter != NULL; pIter = pIter->next) { xmlNode *attr = NULL; nodepriv = pIter->_private; if (!pcmk_any_flags_set(nodepriv->flags, pcmk__xf_deleted|pcmk__xf_dirty)) { continue; } if (change == NULL) { GString *xpath = pcmk__element_xpath(xml); if (xpath != NULL) { change = pcmk__xe_create(patchset, PCMK_XE_CHANGE); crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_MODIFY); crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str); change = pcmk__xe_create(change, PCMK_XE_CHANGE_LIST); g_string_free(xpath, TRUE); } } attr = pcmk__xe_create(change, PCMK_XE_CHANGE_ATTR); crm_xml_add(attr, PCMK_XA_NAME, (const char *) pIter->name); if (nodepriv->flags & pcmk__xf_deleted) { crm_xml_add(attr, PCMK_XA_OPERATION, "unset"); } else { crm_xml_add(attr, PCMK_XA_OPERATION, "set"); value = pcmk__xml_attr_value(pIter); crm_xml_add(attr, PCMK_XA_VALUE, value); } } if (change) { xmlNode *result = NULL; change = pcmk__xe_create(change->parent, PCMK_XE_CHANGE_RESULT); result = pcmk__xe_create(change, (const char *)xml->name); for (pIter = pcmk__xe_first_attr(xml); pIter != NULL; pIter = pIter->next) { nodepriv = pIter->_private; if (!pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) { value = crm_element_value(xml, (const char *) pIter->name); crm_xml_add(result, (const char *)pIter->name, value); } } } // Now recursively do the same for each child node of this node for (cIter = pcmk__xml_first_child(xml); cIter != NULL; cIter = pcmk__xml_next(cIter)) { add_xml_changes_to_patchset(cIter, patchset); } nodepriv = xml->_private; if (patchset && pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) { GString *xpath = pcmk__element_xpath(xml); crm_trace("%s.%s moved to position %d", xml->name, pcmk__xe_id(xml), pcmk__xml_position(xml, pcmk__xf_skip)); if (xpath != NULL) { change = pcmk__xe_create(patchset, PCMK_XE_CHANGE); crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_MOVE); crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str); crm_xml_add_int(change, PCMK_XE_POSITION, pcmk__xml_position(xml, pcmk__xf_deleted)); g_string_free(xpath, TRUE); } } } static bool is_config_change(xmlNode *xml) { GList *gIter = NULL; xml_node_private_t *nodepriv = NULL; xml_doc_private_t *docpriv; xmlNode *config = pcmk__xe_first_child(xml, PCMK_XE_CONFIGURATION, NULL, NULL); if (config) { nodepriv = config->_private; } if ((nodepriv != NULL) && pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) { return TRUE; } if ((xml->doc != NULL) && (xml->doc->_private != NULL)) { docpriv = xml->doc->_private; for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) { pcmk__deleted_xml_t *deleted_obj = gIter->data; if (strstr(deleted_obj->path, "/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION) != NULL) { return TRUE; } } } return FALSE; } static xmlNode * xml_create_patchset_v2(xmlNode *source, xmlNode *target) { int lpc = 0; GList *gIter = NULL; xml_doc_private_t *docpriv; xmlNode *v = NULL; xmlNode *version = NULL; xmlNode *patchset = NULL; const char *vfields[] = { PCMK_XA_ADMIN_EPOCH, PCMK_XA_EPOCH, PCMK_XA_NUM_UPDATES, }; CRM_ASSERT(target); if (!xml_document_dirty(target)) { return NULL; } CRM_ASSERT(target->doc); docpriv = target->doc->_private; patchset = pcmk__xe_create(NULL, PCMK_XE_DIFF); crm_xml_add_int(patchset, PCMK_XA_FORMAT, 2); version = pcmk__xe_create(patchset, PCMK_XE_VERSION); v = pcmk__xe_create(version, PCMK_XE_SOURCE); for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { const char *value = crm_element_value(source, vfields[lpc]); if (value == NULL) { value = "1"; } crm_xml_add(v, vfields[lpc], value); } v = pcmk__xe_create(version, PCMK_XE_TARGET); for (lpc = 0; lpc < PCMK__NELEM(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 = docpriv->deleted_objs; gIter; gIter = gIter->next) { pcmk__deleted_xml_t *deleted_obj = gIter->data; xmlNode *change = pcmk__xe_create(patchset, PCMK_XE_CHANGE); crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_DELETE); crm_xml_add(change, PCMK_XA_PATH, deleted_obj->path); if (deleted_obj->position >= 0) { crm_xml_add_int(change, PCMK_XE_POSITION, deleted_obj->position); } } add_xml_changes_to_patchset(target, patchset); return patchset; } xmlNode * xml_create_patchset(int format, xmlNode *source, xmlNode *target, bool *config_changed, bool manage_version) { bool local_config_changed = false; if (format == 0) { format = 2; } if (format != 2) { crm_err("Unknown patch format: %d", format); return NULL; } xml_acl_disable(target); if (!xml_document_dirty(target)) { crm_trace("No change %d", format); return NULL; /* No change */ } if (config_changed == NULL) { config_changed = &local_config_changed; } *config_changed = is_config_change(target); if (manage_version) { int counter = 0; if (*config_changed) { crm_xml_add(target, PCMK_XA_NUM_UPDATES, "0"); crm_element_value_int(target, PCMK_XA_EPOCH, &counter); crm_xml_add_int(target, PCMK_XA_EPOCH, counter + 1); } else { crm_element_value_int(target, PCMK_XA_NUM_UPDATES, &counter); crm_xml_add_int(target, PCMK_XA_NUM_UPDATES, counter + 1); } } return xml_create_patchset_v2(source, target); } void patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target, bool with_digest) { char *digest = NULL; if ((patch == NULL) || (source == NULL) || (target == NULL) || !with_digest) { return; } /* We should always call xml_accept_changes() before calculating a digest. * Otherwise, with an on-tracking dirty target, we could get a wrong digest. */ CRM_LOG_ASSERT(!xml_document_dirty(target)); digest = pcmk__digest_xml(target, true); crm_xml_add(patch, PCMK__XA_DIGEST, digest); free(digest); return; } // Get CIB versions used for additions and deletions in a patchset bool xml_patch_versions(const xmlNode *patchset, int add[3], int del[3]) { static const char *const vfields[] = { PCMK_XA_ADMIN_EPOCH, PCMK_XA_EPOCH, PCMK_XA_NUM_UPDATES, }; const xmlNode *version = pcmk__xe_first_child(patchset, PCMK_XE_VERSION, NULL, NULL); const xmlNode *source = pcmk__xe_first_child(version, PCMK_XE_SOURCE, NULL, NULL); const xmlNode *target = pcmk__xe_first_child(version, PCMK_XE_TARGET, NULL, NULL); int format = 1; crm_element_value_int(patchset, PCMK_XA_FORMAT, &format); if (format != 2) { crm_err("Unknown patch format: %d", format); return -EINVAL; } if (source != NULL) { for (int i = 0; i < PCMK__NELEM(vfields); i++) { crm_element_value_int(source, vfields[i], &(del[i])); crm_trace("Got %d for del[%s]", del[i], vfields[i]); } } if (target != NULL) { for (int i = 0; i < PCMK__NELEM(vfields); i++) { crm_element_value_int(target, vfields[i], &(add[i])); crm_trace("Got %d for add[%s]", add[i], vfields[i]); } } return pcmk_ok; } /*! * \internal * \brief Check whether patchset can be applied to current CIB * * \param[in] xml Root of current CIB * \param[in] patchset Patchset to check * * \return Standard Pacemaker return code */ static int xml_patch_version_check(const xmlNode *xml, const xmlNode *patchset) { int lpc = 0; bool changed = FALSE; int this[] = { 0, 0, 0 }; int add[] = { 0, 0, 0 }; int del[] = { 0, 0, 0 }; const char *vfields[] = { PCMK_XA_ADMIN_EPOCH, PCMK_XA_EPOCH, PCMK_XA_NUM_UPDATES, }; for (lpc = 0; lpc < PCMK__NELEM(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 < PCMK__NELEM(vfields); lpc++) { del[lpc] = this[lpc]; } xml_patch_versions(patchset, add, del); for (lpc = 0; lpc < PCMK__NELEM(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_rc_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_rc_old_data; } } for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { if (add[lpc] > del[lpc]) { changed = TRUE; } } if (!changed) { crm_notice("Versions did not change in patch %d.%d.%d", add[0], add[1], add[2]); return pcmk_rc_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_rc_ok; } // Return first child matching element name and optionally id or position static xmlNode * first_matching_xml_child(const xmlNode *parent, const char *name, const char *id, int position) { xmlNode *cIter = NULL; for (cIter = pcmk__xml_first_child(parent); cIter != NULL; cIter = pcmk__xml_next(cIter)) { if (strcmp((const char *) cIter->name, name) != 0) { continue; } else if (id) { const char *cid = pcmk__xe_id(cIter); if ((cid == NULL) || (strcmp(cid, id) != 0)) { continue; } } // "position" makes sense only for XML comments for now if ((cIter->type == XML_COMMENT_NODE) && (position >= 0) && (pcmk__xml_position(cIter, pcmk__xf_skip) != 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 * search_v2_xpath(const 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 = pcmk__assert_alloc(key_len, sizeof(char)); section = pcmk__assert_alloc(key_len, sizeof(char)); id = pcmk__assert_alloc(key_len, sizeof(char)); tag = pcmk__assert_alloc(key_len, sizeof(char)); 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, "%[^[][@" PCMK_XA_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_matching_xml_child(target, tag, NULL, current_position); break; case 2: target = first_matching_xml_child(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 { const 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, PCMK_XE_POSITION, &position_a); crm_element_value_int(change_obj_b->change, PCMK_XE_POSITION, &position_b); if (position_a < position_b) { return -1; } else if (position_a > position_b) { return 1; } return 0; } /*! * \internal * \brief Apply a version 2 patchset to an XML node * * \param[in,out] xml XML to apply patchset to * \param[in] patchset Patchset to apply * * \return Standard Pacemaker return code */ static int apply_v2_patchset(xmlNode *xml, const xmlNode *patchset) { int rc = pcmk_rc_ok; const xmlNode *change = NULL; GList *change_objs = NULL; GList *gIter = NULL; for (change = pcmk__xml_first_child(patchset); change != NULL; change = pcmk__xml_next(change)) { xmlNode *match = NULL; const char *op = crm_element_value(change, PCMK_XA_OPERATION); const char *xpath = crm_element_value(change, PCMK_XA_PATH); int position = -1; if (op == NULL) { continue; } crm_trace("Processing %s %s", change->name, op); /* PCMK_VALUE_DELETE changes for XML comments are generated with * PCMK_XE_POSITION */ if (strcmp(op, PCMK_VALUE_DELETE) == 0) { crm_element_value_int(change, PCMK_XE_POSITION, &position); } match = search_v2_xpath(xml, xpath, position); crm_trace("Performing %s on %s with %p", op, xpath, match); if ((match == NULL) && (strcmp(op, PCMK_VALUE_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_rc_diff_failed; continue; } else if (pcmk__str_any_of(op, PCMK_VALUE_CREATE, PCMK_VALUE_MOVE, NULL)) { // Delay the adding of a PCMK_VALUE_CREATE object xml_change_obj_t *change_obj = pcmk__assert_alloc(1, sizeof(xml_change_obj_t)); change_obj->change = change; change_obj->match = match; change_objs = g_list_append(change_objs, change_obj); if (strcmp(op, PCMK_VALUE_MOVE) == 0) { // Temporarily put the PCMK_VALUE_MOVE object after the last sibling if ((match->parent != NULL) && (match->parent->last != NULL)) { xmlAddNextSibling(match->parent->last, match); } } } else if (strcmp(op, PCMK_VALUE_DELETE) == 0) { pcmk__xml_free(match); } else if (strcmp(op, PCMK_VALUE_MODIFY) == 0) { const xmlNode *child = pcmk__xe_first_child(change, PCMK_XE_CHANGE_RESULT, NULL, NULL); const xmlNode *attrs = pcmk__xml_first_child(child); if (attrs == NULL) { rc = ENOMSG; continue; } pcmk__xe_remove_matching_attrs(match, NULL, NULL); // Remove all for (xmlAttrPtr pIter = pcmk__xe_first_attr(attrs); pIter != NULL; pIter = pIter->next) { const char *name = (const char *) pIter->name; const char *value = pcmk__xml_attr_value(pIter); crm_xml_add(match, name, value); } } else { crm_err("Unknown operation: %s", op); rc = pcmk_rc_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, PCMK_XA_OPERATION); xpath = crm_element_value(change, PCMK_XA_PATH); crm_trace("Continue performing %s on %s with %p", op, xpath, match); if (strcmp(op, PCMK_VALUE_CREATE) == 0) { int position = 0; xmlNode *child = NULL; xmlNode *match_child = NULL; match_child = match->children; crm_element_value_int(change, PCMK_XE_POSITION, &position); while ((match_child != NULL) && (position != pcmk__xml_position(match_child, pcmk__xf_skip))) { match_child = match_child->next; } - child = xmlDocCopyNode(change->children, match->doc, 1); - if (child == NULL) { - return ENOMEM; - } + child = pcmk__xml_copy(match, change->children); - if (match_child) { + if (match_child != NULL) { crm_trace("Adding %s at position %d", child->name, position); xmlAddPrevSibling(match_child, child); - } else if (match->last) { - crm_trace("Adding %s at position %d (end)", - child->name, position); - xmlAddNextSibling(match->last, child); - } else { - crm_trace("Adding %s at position %d (first)", + crm_trace("Adding %s at position %d (end)", child->name, position); - CRM_LOG_ASSERT(position == 0); - xmlAddChild(match, child); } - pcmk__xml_mark_created(child); } else if (strcmp(op, PCMK_VALUE_MOVE) == 0) { int position = 0; crm_element_value_int(change, PCMK_XE_POSITION, &position); if (position != pcmk__xml_position(match, pcmk__xf_skip)) { xmlNode *match_child = NULL; int p = position; if (p > pcmk__xml_position(match, pcmk__xf_skip)) { p++; // Skip ourselves } CRM_ASSERT(match->parent != NULL); match_child = match->parent->children; while ((match_child != NULL) && (p != pcmk__xml_position(match_child, pcmk__xf_skip))) { match_child = match_child->next; } crm_trace("Moving %s to position %d (was %d, prev %p, %s %p)", match->name, position, pcmk__xml_position(match, pcmk__xf_skip), 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 != pcmk__xml_position(match, pcmk__xf_skip)) { crm_err("Moved %s.%s to position %d instead of %d (%p)", match->name, pcmk__xe_id(match), pcmk__xml_position(match, pcmk__xf_skip), position, match->prev); rc = pcmk_rc_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 = NULL; if (patchset == NULL) { return rc; } pcmk__log_xml_patchset(LOG_TRACE, patchset); if (check_version) { rc = pcmk_rc2legacy(xml_patch_version_check(xml, patchset)); if (rc != pcmk_ok) { return rc; } } digest = crm_element_value(patchset, PCMK__XA_DIGEST); if (digest != NULL) { /* Make original XML available for logging in case result doesn't have * expected digest */ pcmk__if_tracing(old = pcmk__xml_copy(NULL, xml), {}); } if (rc == pcmk_ok) { crm_element_value_int(patchset, PCMK_XA_FORMAT, &format); if (format != 2) { crm_err("Unknown patch format: %d", format); rc = -EINVAL; } else { rc = pcmk_rc2legacy(apply_v2_patchset(xml, patchset)); } } if ((rc == pcmk_ok) && (digest != NULL)) { char *new_digest = NULL; new_digest = pcmk__digest_xml(xml, true); if (!pcmk__str_eq(new_digest, digest, pcmk__str_casei)) { crm_info("v%d digest mis-match: expected %s, calculated %s", format, digest, new_digest); rc = -pcmk_err_diff_failed; pcmk__if_tracing( { 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("v%d digest matched: expected %s, calculated %s", format, digest, new_digest); } free(new_digest); } pcmk__xml_free(old); return rc; } bool pcmk__cib_element_in_patchset(const xmlNode *patchset, const char *element) { const char *element_xpath = pcmk__cib_abs_xpath_for(element); const char *parent_xpath = pcmk_cib_parent_name_for(element); char *element_regex = NULL; bool rc = false; int format = 1; CRM_ASSERT(patchset != NULL); crm_element_value_int(patchset, PCMK_XA_FORMAT, &format); if (format != 2) { crm_warn("Unknown patch format: %d", format); return false; } CRM_CHECK(element_xpath != NULL, return false); // Unsupported element /* Matches if and only if element_xpath is part of a changed path * (supported values for element never contain XML IDs with schema * validation enabled) * * @TODO Use POSIX word boundary instead of (/|$), if it works: * https://www.regular-expressions.info/wordboundaries.html. */ element_regex = crm_strdup_printf("^%s(/|$)", element_xpath); for (const xmlNode *change = pcmk__xe_first_child(patchset, PCMK_XE_CHANGE, NULL, NULL); change != NULL; change = pcmk__xe_next_same(change)) { const char *op = crm_element_value(change, PCMK_XA_OPERATION); const char *diff_xpath = crm_element_value(change, PCMK_XA_PATH); if (pcmk__str_eq(diff_xpath, element_regex, pcmk__str_regex)) { // Change to an existing element rc = true; break; } if (pcmk__str_eq(op, PCMK_VALUE_CREATE, pcmk__str_none) && pcmk__str_eq(diff_xpath, parent_xpath, pcmk__str_none) && pcmk__xe_is(pcmk__xe_first_child(change, NULL, NULL, NULL), element)) { // Newly added element rc = true; break; } } free(element_regex); return rc; } diff --git a/lib/common/schemas.c b/lib/common/schemas.c index 9a9327326e..0470f99559 100644 --- a/lib/common/schemas.c +++ b/lib/common/schemas.c @@ -1,1547 +1,1553 @@ /* * Copyright 2004-2024 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 /* PCMK__XML_LOG_BASE */ #include "crmcommon_private.h" #define SCHEMA_ZERO { .v = { 0, 0 } } #define schema_strdup_printf(prefix, version, suffix) \ crm_strdup_printf(prefix "%u.%u" suffix, (version).v[0], (version).v[1]) typedef struct { xmlRelaxNGPtr rng; xmlRelaxNGValidCtxtPtr valid; xmlRelaxNGParserCtxtPtr parser; } relaxng_ctx_cache_t; static GList *known_schemas = NULL; static bool initialized = false; static bool silent_logging = FALSE; static void G_GNUC_PRINTF(2, 3) xml_log(int priority, const char *fmt, ...) { va_list ap; va_start(ap, fmt); if (silent_logging == FALSE) { /* XXX should not this enable dechunking as well? */ PCMK__XML_LOG_BASE(priority, FALSE, 0, NULL, fmt, ap); } va_end(ap); } static int xml_latest_schema_index(void) { /* This function assumes that pcmk__schema_init() has been called * beforehand, so we have at least two schemas (one real schema and the * "none" schema). * * @COMPAT: The "none" schema is deprecated since 2.1.8. * Update this when we drop that schema. */ return g_list_length(known_schemas) - 2; } /*! * \internal * \brief Return the schema entry of the highest-versioned schema * * \return Schema entry of highest-versioned schema */ static GList * get_highest_schema(void) { /* The highest numerically versioned schema is the one before none * * @COMPAT none is deprecated since 2.1.8 */ GList *entry = pcmk__get_schema("none"); CRM_ASSERT((entry != NULL) && (entry->prev != NULL)); return entry->prev; } /*! * \internal * \brief Return the name of the highest-versioned schema * * \return Name of highest-versioned schema (or NULL on error) */ const char * pcmk__highest_schema_name(void) { GList *entry = get_highest_schema(); return ((pcmk__schema_t *)(entry->data))->name; } /*! * \internal * \brief Find first entry of highest major schema version series * * \return Schema entry of first schema with highest major version */ GList * pcmk__find_x_0_schema(void) { #if defined(PCMK__UNIT_TESTING) /* If we're unit testing, this can't be static because it'll stick * around from one test run to the next. It needs to be cleared out * every time. */ GList *x_0_entry = NULL; #else static GList *x_0_entry = NULL; #endif pcmk__schema_t *highest_schema = NULL; if (x_0_entry != NULL) { return x_0_entry; } x_0_entry = get_highest_schema(); highest_schema = x_0_entry->data; for (GList *iter = x_0_entry->prev; iter != NULL; iter = iter->prev) { pcmk__schema_t *schema = iter->data; /* We've found a schema in an older major version series. Return * the index of the first one in the same major version series as * the highest schema. */ if (schema->version.v[0] < highest_schema->version.v[0]) { x_0_entry = iter->next; break; } /* We're out of list to examine. This probably means there was only * one major version series, so return the first schema entry. */ if (iter->prev == NULL) { x_0_entry = known_schemas->data; break; } } return x_0_entry; } static inline bool version_from_filename(const char *filename, pcmk__schema_version_t *version) { if (pcmk__ends_with(filename, ".rng")) { return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2; } else { return sscanf(filename, "pacemaker-%hhu.%hhu", &(version->v[0]), &(version->v[1])) == 2; } } static int schema_filter(const struct dirent *a) { int rc = 0; pcmk__schema_version_t version = SCHEMA_ZERO; if (strstr(a->d_name, "pacemaker-") != a->d_name) { /* crm_trace("%s - wrong prefix", a->d_name); */ } else if (!pcmk__ends_with_ext(a->d_name, ".rng")) { /* crm_trace("%s - wrong suffix", a->d_name); */ } else if (!version_from_filename(a->d_name, &version)) { /* crm_trace("%s - wrong format", a->d_name); */ } else { /* crm_debug("%s - candidate", a->d_name); */ rc = 1; } return rc; } static int schema_cmp(pcmk__schema_version_t a_version, pcmk__schema_version_t b_version) { for (int i = 0; i < 2; ++i) { if (a_version.v[i] < b_version.v[i]) { return -1; } else if (a_version.v[i] > b_version.v[i]) { return 1; } } return 0; } static int schema_cmp_directory(const struct dirent **a, const struct dirent **b) { pcmk__schema_version_t a_version = SCHEMA_ZERO; pcmk__schema_version_t b_version = SCHEMA_ZERO; if (!version_from_filename(a[0]->d_name, &a_version) || !version_from_filename(b[0]->d_name, &b_version)) { // Shouldn't be possible, but makes static analysis happy return 0; } return schema_cmp(a_version, b_version); } /*! * \internal * \brief Add given schema + auxiliary data to internal bookkeeping. */ static void add_schema(enum pcmk__schema_validator validator, const pcmk__schema_version_t *version, const char *name, GList *transforms) { pcmk__schema_t *schema = NULL; schema = pcmk__assert_alloc(1, sizeof(pcmk__schema_t)); schema->validator = validator; schema->version.v[0] = version->v[0]; schema->version.v[1] = version->v[1]; schema->transforms = transforms; // schema->schema_index is set after all schemas are loaded and sorted if (version->v[0] || version->v[1]) { schema->name = schema_strdup_printf("pacemaker-", *version, ""); } else { schema->name = pcmk__str_copy(name); } known_schemas = g_list_prepend(known_schemas, schema); } static void wrap_libxslt(bool finalize) { static xsltSecurityPrefsPtr secprefs; int ret = 0; /* security framework preferences */ if (!finalize) { CRM_ASSERT(secprefs == NULL); secprefs = xsltNewSecurityPrefs(); ret = xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_FILE, xsltSecurityForbid) | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_CREATE_DIRECTORY, xsltSecurityForbid) | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_READ_NETWORK, xsltSecurityForbid) | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_NETWORK, xsltSecurityForbid); if (ret != 0) { return; } } else { xsltFreeSecurityPrefs(secprefs); secprefs = NULL; } /* cleanup only */ if (finalize) { xsltCleanupGlobals(); } } /*! * \internal * \brief Check whether a directory entry matches the upgrade XSLT pattern * * \param[in] entry Directory entry whose filename to check * * \return 1 if the entry's filename is of the form * upgrade-X.Y-ORDER.xsl, or 0 otherwise */ static int transform_filter(const struct dirent *entry) { return pcmk__str_eq(entry->d_name, "upgrade-[[:digit:]]+.[[:digit:]]+-[[:digit:]]+.xsl", pcmk__str_regex)? 1 : 0; } /*! * \internal * \brief Free a list of XSLT transform struct dirent objects * * \param[in,out] data List to free */ static void free_transform_list(void *data) { g_list_free_full((GList *) data, free); } /*! * \internal * \brief Load names of upgrade XSLT stylesheets from a directory into a table * * Stylesheets must have names of the form "upgrade-X.Y-order.xsl", where: * * X is the schema major version * * Y is the schema minor version * * ORDER is the order in which the stylesheet occurs in the transform pipeline * * \param[in] dir Directory containing XSLT stylesheets * * \return Table with schema version as key and \c GList of associated transform * files (as struct dirent) as value */ static GHashTable * load_transforms_from_dir(const char *dir) { struct dirent **namelist = NULL; int num_matches = scandir(dir, &namelist, transform_filter, versionsort); GHashTable *transforms = pcmk__strkey_table(free, free_transform_list); for (int i = 0; i < num_matches; i++) { pcmk__schema_version_t version = SCHEMA_ZERO; int order = 0; // Placeholder only if (sscanf(namelist[i]->d_name, "upgrade-%hhu.%hhu-%d.xsl", &(version.v[0]), &(version.v[1]), &order) == 3) { char *version_s = crm_strdup_printf("%hhu.%hhu", version.v[0], version.v[1]); GList *list = g_hash_table_lookup(transforms, version_s); if (list == NULL) { /* Prepend is more efficient. However, there won't be many of * these, and we want them to remain sorted by version. It's not * worth reversing all the lists at the end. * * Avoid calling g_hash_table_insert() if the list already * exists. Otherwise free_transform_list() gets called on it. */ list = g_list_append(list, namelist[i]); g_hash_table_insert(transforms, version_s, list); } else { list = g_list_append(list, namelist[i]); free(version_s); } } else { // Sanity only, should never happen thanks to transform_filter() free(namelist[i]); } } free(namelist); return transforms; } void pcmk__load_schemas_from_dir(const char *dir) { int lpc, max; struct dirent **namelist = NULL; GHashTable *transforms = NULL; max = scandir(dir, &namelist, schema_filter, schema_cmp_directory); if (max < 0) { crm_warn("Could not load schemas from %s: %s", dir, strerror(errno)); return; } // Look for any upgrade transforms in the same directory transforms = load_transforms_from_dir(dir); for (lpc = 0; lpc < max; lpc++) { pcmk__schema_version_t version = SCHEMA_ZERO; if (version_from_filename(namelist[lpc]->d_name, &version)) { char *version_s = crm_strdup_printf("%hhu.%hhu", version.v[0], version.v[1]); char *orig_key = NULL; GList *transform_list = NULL; // The schema becomes the owner of transform_list g_hash_table_lookup_extended(transforms, version_s, (gpointer *) &orig_key, (gpointer *) &transform_list); g_hash_table_steal(transforms, version_s); add_schema(pcmk__schema_validator_rng, &version, NULL, transform_list); free(version_s); free(orig_key); } else { // Shouldn't be possible, but makes static analysis happy crm_warn("Skipping schema '%s': could not parse version", namelist[lpc]->d_name); } } for (lpc = 0; lpc < max; lpc++) { free(namelist[lpc]); } free(namelist); g_hash_table_destroy(transforms); } static gint schema_sort_GCompareFunc(gconstpointer a, gconstpointer b) { const pcmk__schema_t *schema_a = a; const pcmk__schema_t *schema_b = b; // @COMPAT The "none" schema is deprecated since 2.1.8 if (pcmk__str_eq(schema_a->name, PCMK_VALUE_NONE, pcmk__str_none)) { return 1; } else if (pcmk__str_eq(schema_b->name, PCMK_VALUE_NONE, pcmk__str_none)) { return -1; } else { return schema_cmp(schema_a->version, schema_b->version); } } /*! * \internal * \brief Sort the list of known schemas such that all pacemaker-X.Y are in * version order, then "none" * * This function should be called whenever additional schemas are loaded using * \c pcmk__load_schemas_from_dir(), after the initial sets in * \c pcmk__schema_init(). */ void pcmk__sort_schemas(void) { known_schemas = g_list_sort(known_schemas, schema_sort_GCompareFunc); } /*! * \internal * \brief Load pacemaker schemas into cache * * \note This currently also serves as an entry point for the * generic initialization of the libxslt library. */ void pcmk__schema_init(void) { if (!initialized) { const char *remote_schema_dir = pcmk__remote_schema_dir(); char *base = pcmk__xml_artefact_root(pcmk__xml_artefact_ns_legacy_rng); const pcmk__schema_version_t zero = SCHEMA_ZERO; int schema_index = 0; initialized = true; wrap_libxslt(false); pcmk__load_schemas_from_dir(base); pcmk__load_schemas_from_dir(remote_schema_dir); free(base); // @COMPAT Deprecated since 2.1.8 add_schema(pcmk__schema_validator_none, &zero, PCMK_VALUE_NONE, NULL); /* add_schema() prepends items to the list, so in the simple case, this * just reverses the list. However if there were any remote schemas, * sorting is necessary. */ pcmk__sort_schemas(); // Now set the schema indexes and log the final result for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { pcmk__schema_t *schema = iter->data; crm_debug("Loaded schema %d: %s", schema_index, schema->name); schema->schema_index = schema_index++; } } } static bool validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, void *error_handler_context, const char *relaxng_file, relaxng_ctx_cache_t **cached_ctx) { int rc = 0; bool valid = true; relaxng_ctx_cache_t *ctx = NULL; CRM_CHECK(doc != NULL, return false); CRM_CHECK(relaxng_file != NULL, return false); if (cached_ctx && *cached_ctx) { ctx = *cached_ctx; } else { crm_debug("Creating RNG parser context"); ctx = pcmk__assert_alloc(1, sizeof(relaxng_ctx_cache_t)); ctx->parser = xmlRelaxNGNewParserCtxt(relaxng_file); CRM_CHECK(ctx->parser != NULL, goto cleanup); if (error_handler) { xmlRelaxNGSetParserErrors(ctx->parser, (xmlRelaxNGValidityErrorFunc) error_handler, (xmlRelaxNGValidityWarningFunc) error_handler, error_handler_context); } else { xmlRelaxNGSetParserErrors(ctx->parser, (xmlRelaxNGValidityErrorFunc) fprintf, (xmlRelaxNGValidityWarningFunc) fprintf, stderr); } ctx->rng = xmlRelaxNGParse(ctx->parser); CRM_CHECK(ctx->rng != NULL, crm_err("Could not find/parse %s", relaxng_file); goto cleanup); ctx->valid = xmlRelaxNGNewValidCtxt(ctx->rng); CRM_CHECK(ctx->valid != NULL, goto cleanup); if (error_handler) { xmlRelaxNGSetValidErrors(ctx->valid, (xmlRelaxNGValidityErrorFunc) error_handler, (xmlRelaxNGValidityWarningFunc) error_handler, error_handler_context); } else { xmlRelaxNGSetValidErrors(ctx->valid, (xmlRelaxNGValidityErrorFunc) fprintf, (xmlRelaxNGValidityWarningFunc) fprintf, stderr); } } rc = xmlRelaxNGValidateDoc(ctx->valid, doc); if (rc > 0) { valid = false; } else if (rc < 0) { crm_err("Internal libxml error during validation"); } cleanup: if (cached_ctx) { *cached_ctx = ctx; } else { if (ctx->parser != NULL) { xmlRelaxNGFreeParserCtxt(ctx->parser); } if (ctx->valid != NULL) { xmlRelaxNGFreeValidCtxt(ctx->valid); } if (ctx->rng != NULL) { xmlRelaxNGFree(ctx->rng); } free(ctx); } return valid; } static void free_schema(gpointer data) { pcmk__schema_t *schema = data; relaxng_ctx_cache_t *ctx = NULL; switch (schema->validator) { case pcmk__schema_validator_none: // not cached break; case pcmk__schema_validator_rng: // cached ctx = (relaxng_ctx_cache_t *) schema->cache; if (ctx == NULL) { break; } if (ctx->parser != NULL) { xmlRelaxNGFreeParserCtxt(ctx->parser); } if (ctx->valid != NULL) { xmlRelaxNGFreeValidCtxt(ctx->valid); } if (ctx->rng != NULL) { xmlRelaxNGFree(ctx->rng); } free(ctx); schema->cache = NULL; break; } free(schema->name); g_list_free_full(schema->transforms, free); free(schema); } /*! * \internal * \brief Clean up global memory associated with XML schemas */ void pcmk__schema_cleanup(void) { if (known_schemas != NULL) { g_list_free_full(known_schemas, free_schema); known_schemas = NULL; } initialized = false; wrap_libxslt(true); } /*! * \internal * \brief Get schema list entry corresponding to a schema name * * \param[in] name Name of schema to get * * \return Schema list entry corresponding to \p name, or NULL if unknown */ GList * pcmk__get_schema(const char *name) { // @COMPAT Not specifying a schema name is deprecated since 2.1.8 if (name == NULL) { name = PCMK_VALUE_NONE; } for (GList *iter = known_schemas; iter != NULL; iter = iter->next) { pcmk__schema_t *schema = iter->data; if (pcmk__str_eq(name, schema->name, pcmk__str_none)) { return iter; } } return NULL; } /*! * \internal * \brief Compare two schema version numbers given the schema names * * \param[in] schema1 Name of first schema to compare * \param[in] schema2 Name of second schema to compare * * \return Standard comparison result (negative integer if \p schema1 has the * lower version number, positive integer if \p schema1 has the higher * version number, of 0 if the version numbers are equal) */ int pcmk__cmp_schemas_by_name(const char *schema1_name, const char *schema2_name) { GList *entry1 = pcmk__get_schema(schema1_name); GList *entry2 = pcmk__get_schema(schema2_name); if (entry1 == NULL) { return (entry2 == NULL)? 0 : -1; } else if (entry2 == NULL) { return 1; } else { pcmk__schema_t *schema1 = entry1->data; pcmk__schema_t *schema2 = entry2->data; return schema1->schema_index - schema2->schema_index; } } static bool validate_with(xmlNode *xml, pcmk__schema_t *schema, xmlRelaxNGValidityErrorFunc error_handler, void *error_handler_context) { bool valid = false; char *file = NULL; relaxng_ctx_cache_t **cache = NULL; if (schema == NULL) { return false; } if (schema->validator == pcmk__schema_validator_none) { return true; } file = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, schema->name); crm_trace("Validating with %s (type=%d)", pcmk__s(file, "missing schema"), schema->validator); switch (schema->validator) { case pcmk__schema_validator_rng: cache = (relaxng_ctx_cache_t **) &(schema->cache); valid = validate_with_relaxng(xml->doc, error_handler, error_handler_context, file, cache); break; default: crm_err("Unknown validator type: %d", schema->validator); break; } free(file); return valid; } static bool validate_with_silent(xmlNode *xml, pcmk__schema_t *schema) { bool rc, sl_backup = silent_logging; silent_logging = TRUE; rc = validate_with(xml, schema, (xmlRelaxNGValidityErrorFunc) xml_log, GUINT_TO_POINTER(LOG_ERR)); silent_logging = sl_backup; return rc; } bool pcmk__validate_xml(xmlNode *xml_blob, const char *validation, xmlRelaxNGValidityErrorFunc error_handler, void *error_handler_context) { GList *entry = NULL; pcmk__schema_t *schema = NULL; CRM_CHECK((xml_blob != NULL) && (xml_blob->doc != NULL), return false); if (validation == NULL) { validation = crm_element_value(xml_blob, PCMK_XA_VALIDATE_WITH); } pcmk__warn_if_schema_deprecated(validation); // @COMPAT Not specifying a schema name is deprecated since 2.1.8 if (validation == NULL) { bool valid = false; for (entry = known_schemas; entry != NULL; entry = entry->next) { schema = entry->data; if (validate_with(xml_blob, schema, NULL, NULL)) { valid = true; crm_xml_add(xml_blob, PCMK_XA_VALIDATE_WITH, schema->name); crm_info("XML validated against %s", schema->name); } } return valid; } entry = pcmk__get_schema(validation); if (entry == NULL) { pcmk__config_err("Cannot validate CIB with " PCMK_XA_VALIDATE_WITH " set to an unknown schema such as '%s' (manually" " edit to use a known schema)", validation); return false; } schema = entry->data; return validate_with(xml_blob, schema, error_handler, error_handler_context); } /*! * \internal * \brief Validate XML using its configured schema (and send errors to logs) * * \param[in] xml XML to validate * * \return true if XML validates, otherwise false */ bool pcmk__configured_schema_validates(xmlNode *xml) { return pcmk__validate_xml(xml, NULL, (xmlRelaxNGValidityErrorFunc) xml_log, GUINT_TO_POINTER(LOG_ERR)); } /* With this arrangement, an attempt to identify the message severity as explicitly signalled directly from XSLT is performed in rather a smart way (no reliance on formatting string + arguments being always specified as ["%s", purposeful_string], as it can also be ["%s: %s", some_prefix, purposeful_string] etc. so every argument pertaining %s specifier is investigated), and if such a mark found, the respective level is determined and, when the messages are to go to the native logs, the mark itself gets dropped (by the means of string shift). NOTE: whether the native logging is the right sink is decided per the ctx parameter -- NULL denotes this case, otherwise it carries a pointer to the numeric expression of the desired target logging level (messages with higher level will be suppressed) NOTE: on some architectures, this string shift may not have any effect, but that's an acceptable tradeoff The logging level for not explicitly designated messages (suspicious, likely internal errors or some runaways) is LOG_WARNING. */ static void G_GNUC_PRINTF(2, 3) cib_upgrade_err(void *ctx, const char *fmt, ...) { va_list ap, aq; char *arg_cur; bool found = FALSE; const char *fmt_iter = fmt; uint8_t msg_log_level = LOG_WARNING; /* default for runaway messages */ const unsigned * log_level = (const unsigned *) ctx; enum { escan_seennothing, escan_seenpercent, } scan_state = escan_seennothing; va_start(ap, fmt); va_copy(aq, ap); while (!found && *fmt_iter != '\0') { /* while casing schema borrowed from libqb:qb_vsnprintf_serialize */ switch (*fmt_iter++) { case '%': if (scan_state == escan_seennothing) { scan_state = escan_seenpercent; } else if (scan_state == escan_seenpercent) { scan_state = escan_seennothing; } break; case 's': if (scan_state == escan_seenpercent) { scan_state = escan_seennothing; arg_cur = va_arg(aq, char *); if (arg_cur != NULL) { switch (arg_cur[0]) { case 'W': if (!strncmp(arg_cur, "WARNING: ", sizeof("WARNING: ") - 1)) { msg_log_level = LOG_WARNING; } if (ctx == NULL) { memmove(arg_cur, arg_cur + sizeof("WARNING: ") - 1, strlen(arg_cur + sizeof("WARNING: ") - 1) + 1); } found = TRUE; break; case 'I': if (!strncmp(arg_cur, "INFO: ", sizeof("INFO: ") - 1)) { msg_log_level = LOG_INFO; } if (ctx == NULL) { memmove(arg_cur, arg_cur + sizeof("INFO: ") - 1, strlen(arg_cur + sizeof("INFO: ") - 1) + 1); } found = TRUE; break; case 'D': if (!strncmp(arg_cur, "DEBUG: ", sizeof("DEBUG: ") - 1)) { msg_log_level = LOG_DEBUG; } if (ctx == NULL) { memmove(arg_cur, arg_cur + sizeof("DEBUG: ") - 1, strlen(arg_cur + sizeof("DEBUG: ") - 1) + 1); } found = TRUE; break; } } } break; case '#': case '-': case ' ': case '+': case '\'': case 'I': case '.': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '*': break; case 'l': case 'z': case 't': case 'j': case 'd': case 'i': case 'o': case 'u': case 'x': case 'X': case 'e': case 'E': case 'f': case 'F': case 'g': case 'G': case 'a': case 'A': case 'c': case 'p': if (scan_state == escan_seenpercent) { (void) va_arg(aq, void *); /* skip forward */ scan_state = escan_seennothing; } break; default: scan_state = escan_seennothing; break; } } if (log_level != NULL) { /* intention of the following offset is: cibadmin -V -> start showing INFO labelled messages */ if (*log_level + 4 >= msg_log_level) { vfprintf(stderr, fmt, ap); } } else { PCMK__XML_LOG_BASE(msg_log_level, TRUE, 0, "CIB upgrade: ", fmt, ap); } va_end(aq); va_end(ap); } /*! * \internal * \brief Apply a single XSL transformation to given XML * * \param[in] xml XML to transform * \param[in] transform XSL name * \param[in] to_logs If false, certain validation errors will be sent to * stderr rather than logged * * \return Transformed XML on success, otherwise NULL */ static xmlNode * apply_transformation(const xmlNode *xml, const char *transform, gboolean to_logs) { char *xform = NULL; xmlNode *out = NULL; xmlDocPtr res = NULL; xsltStylesheet *xslt = NULL; xform = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, transform); /* for capturing, e.g., what's emitted via */ if (to_logs) { xsltSetGenericErrorFunc(NULL, cib_upgrade_err); } else { xsltSetGenericErrorFunc(&crm_log_level, cib_upgrade_err); } xslt = xsltParseStylesheetFile((pcmkXmlStr) xform); CRM_CHECK(xslt != NULL, goto cleanup); + /* Caller allocates private data for final result document. Intermediate + * result documents are temporary and don't need private data. + */ res = xsltApplyStylesheet(xslt, xml->doc, NULL); CRM_CHECK(res != NULL, goto cleanup); xsltSetGenericErrorFunc(NULL, NULL); /* restore default one */ out = xmlDocGetRootElement(res); cleanup: if (xslt) { xsltFreeStylesheet(xslt); } free(xform); return out; } /*! * \internal * \brief Perform all transformations needed to upgrade XML to next schema * * \param[in] input_xml XML to transform * \param[in] schema_index Index of schema that successfully validates * \p original_xml * \param[in] to_logs If false, certain validation errors will be sent to * stderr rather than logged * * \return XML result of schema transforms if successful, otherwise NULL */ static xmlNode * apply_upgrade(const xmlNode *input_xml, int schema_index, gboolean to_logs) { pcmk__schema_t *schema = g_list_nth_data(known_schemas, schema_index); pcmk__schema_t *upgraded_schema = g_list_nth_data(known_schemas, schema_index + 1); xmlNode *old_xml = NULL; xmlNode *new_xml = NULL; xmlRelaxNGValidityErrorFunc error_handler = NULL; CRM_ASSERT((schema != NULL) && (upgraded_schema != NULL)); if (to_logs) { error_handler = (xmlRelaxNGValidityErrorFunc) xml_log; } for (GList *iter = schema->transforms; iter != NULL; iter = iter->next) { const struct dirent *entry = iter->data; const char *transform = entry->d_name; crm_debug("Upgrading schema from %s to %s: applying XSL transform %s", schema->name, upgraded_schema->name, transform); new_xml = apply_transformation(input_xml, transform, to_logs); pcmk__xml_free(old_xml); if (new_xml == NULL) { crm_err("XSL transform %s failed, aborting upgrade", transform); return NULL; } input_xml = new_xml; old_xml = new_xml; } + // Final result document from upgrade pipeline needs private data + pcmk__xml_new_private_data((xmlNode *) new_xml->doc); + // Ensure result validates with its new schema if (!validate_with(new_xml, upgraded_schema, error_handler, GUINT_TO_POINTER(LOG_ERR))) { crm_err("Schema upgrade from %s to %s failed: " "XSL transform pipeline produced an invalid configuration", schema->name, upgraded_schema->name); crm_log_xml_debug(new_xml, "bad-transform-result"); pcmk__xml_free(new_xml); return NULL; } crm_info("Schema upgrade from %s to %s succeeded", schema->name, upgraded_schema->name); return new_xml; } /*! * \internal * \brief Get the schema list entry corresponding to XML configuration * * \param[in] xml CIB XML to check * * \return List entry of schema configured in \p xml */ static GList * get_configured_schema(const xmlNode *xml) { const char *schema_name = crm_element_value(xml, PCMK_XA_VALIDATE_WITH); pcmk__warn_if_schema_deprecated(schema_name); if (schema_name == NULL) { return NULL; } return pcmk__get_schema(schema_name); } /*! * \brief Update CIB XML to latest schema that validates it * * \param[in,out] xml XML to update (may be freed and replaced * after being transformed) * \param[in] max_schema_name If not NULL, do not update \p xml to any * schema later than this one * \param[in] transform If false, do not update \p xml to any schema * that requires an XSL transform * \param[in] to_logs If false, certain validation errors will be * sent to stderr rather than logged * * \return Standard Pacemaker return code */ int pcmk__update_schema(xmlNode **xml, const char *max_schema_name, bool transform, bool to_logs) { int max_stable_schemas = xml_latest_schema_index(); int max_schema_index = 0; int rc = pcmk_rc_ok; GList *entry = NULL; pcmk__schema_t *best_schema = NULL; pcmk__schema_t *original_schema = NULL; xmlRelaxNGValidityErrorFunc error_handler = to_logs ? (xmlRelaxNGValidityErrorFunc) xml_log : NULL; CRM_CHECK((xml != NULL) && (*xml != NULL) && ((*xml)->doc != NULL), return EINVAL); if (max_schema_name != NULL) { GList *max_entry = pcmk__get_schema(max_schema_name); if (max_entry != NULL) { pcmk__schema_t *max_schema = max_entry->data; max_schema_index = max_schema->schema_index; } } if ((max_schema_index < 1) || (max_schema_index > max_stable_schemas)) { max_schema_index = max_stable_schemas; } entry = get_configured_schema(*xml); if (entry == NULL) { // @COMPAT Not specifying a schema name is deprecated since 2.1.8 entry = known_schemas; } else { original_schema = entry->data; if (original_schema->schema_index >= max_schema_index) { return pcmk_rc_ok; } } for (; entry != NULL; entry = entry->next) { pcmk__schema_t *current_schema = entry->data; xmlNode *upgrade = NULL; if (current_schema->schema_index > max_schema_index) { break; } if (!validate_with(*xml, current_schema, error_handler, GUINT_TO_POINTER(LOG_ERR))) { crm_debug("Schema %s does not validate", current_schema->name); if (best_schema != NULL) { /* we've satisfied the validation, no need to check further */ break; } rc = pcmk_rc_schema_validation; continue; // Try again with the next higher schema } crm_debug("Schema %s validates", current_schema->name); rc = pcmk_rc_ok; best_schema = current_schema; if (current_schema->schema_index == max_schema_index) { break; // No further transformations possible } if (!transform || (current_schema->transforms == NULL) || validate_with_silent(*xml, entry->next->data)) { /* The next schema either doesn't require a transform or validates * successfully even without the transform. Skip the transform and * try the next schema with the same XML. */ continue; } upgrade = apply_upgrade(*xml, current_schema->schema_index, to_logs); if (upgrade == NULL) { /* The transform failed, so this schema can't be used. Later * schemas are unlikely to validate, but try anyway until we * run out of options. */ rc = pcmk_rc_transform_failed; } else { best_schema = current_schema; pcmk__xml_free(*xml); *xml = upgrade; } } if (best_schema != NULL) { if ((original_schema == NULL) || (best_schema->schema_index > original_schema->schema_index)) { crm_info("%s the configuration schema to %s", (transform? "Transformed" : "Upgraded"), best_schema->name); crm_xml_add(*xml, PCMK_XA_VALIDATE_WITH, best_schema->name); } } return rc; } int pcmk_update_configured_schema(xmlNode **xml) { return pcmk__update_configured_schema(xml, true); } /*! * \brief Update XML from its configured schema to the latest major series * * \param[in,out] xml XML to update * \param[in] to_logs If false, certain validation errors will be * sent to stderr rather than logged * * \return Standard Pacemaker return code */ int pcmk__update_configured_schema(xmlNode **xml, bool to_logs) { int rc = pcmk_rc_ok; char *original_schema_name = NULL; // @COMPAT Not specifying a schema name is deprecated since 2.1.8 const char *effective_original_name = "the first"; int orig_version = -1; pcmk__schema_t *x_0_schema = pcmk__find_x_0_schema()->data; GList *entry = NULL; CRM_CHECK(xml != NULL, return EINVAL); original_schema_name = crm_element_value_copy(*xml, PCMK_XA_VALIDATE_WITH); pcmk__warn_if_schema_deprecated(original_schema_name); entry = pcmk__get_schema(original_schema_name); if (entry != NULL) { pcmk__schema_t *original_schema = entry->data; effective_original_name = original_schema->name; orig_version = original_schema->schema_index; } if (orig_version < x_0_schema->schema_index) { // Current configuration schema is not acceptable, try to update xmlNode *converted = NULL; const char *new_schema_name = NULL; pcmk__schema_t *schema = NULL; entry = NULL; converted = pcmk__xml_copy(NULL, *xml); if (pcmk__update_schema(&converted, NULL, true, to_logs) == pcmk_rc_ok) { new_schema_name = crm_element_value(converted, PCMK_XA_VALIDATE_WITH); entry = pcmk__get_schema(new_schema_name); } schema = (entry == NULL)? NULL : entry->data; if ((schema == NULL) || (schema->schema_index < x_0_schema->schema_index)) { // Updated configuration schema is still not acceptable if ((orig_version == -1) || (schema == NULL) || (schema->schema_index < orig_version)) { // We couldn't validate any schema at all if (to_logs) { pcmk__config_err("Cannot upgrade configuration (claiming " "%s schema) to at least %s because it " "does not validate with any schema from " "%s to the latest", pcmk__s(original_schema_name, "no"), x_0_schema->name, effective_original_name); } else { fprintf(stderr, "Cannot upgrade configuration (claiming " "%s schema) to at least %s because it " "does not validate with any schema from " "%s to the latest\n", pcmk__s(original_schema_name, "no"), x_0_schema->name, effective_original_name); } } else { // We updated configuration successfully, but still too low if (to_logs) { pcmk__config_err("Cannot upgrade configuration (claiming " "%s schema) to at least %s because it " "would not upgrade past %s", pcmk__s(original_schema_name, "no"), x_0_schema->name, pcmk__s(new_schema_name, "unspecified version")); } else { fprintf(stderr, "Cannot upgrade configuration (claiming " "%s schema) to at least %s because it " "would not upgrade past %s\n", pcmk__s(original_schema_name, "no"), x_0_schema->name, pcmk__s(new_schema_name, "unspecified version")); } } pcmk__xml_free(converted); converted = NULL; rc = pcmk_rc_transform_failed; } else { // Updated configuration schema is acceptable pcmk__xml_free(*xml); *xml = converted; if (schema->schema_index < xml_latest_schema_index()) { if (to_logs) { pcmk__config_warn("Configuration with %s schema was " "internally upgraded to acceptable (but " "not most recent) %s", pcmk__s(original_schema_name, "no"), schema->name); } } else if (to_logs) { crm_info("Configuration with %s schema was internally " "upgraded to latest version %s", pcmk__s(original_schema_name, "no"), schema->name); } } } else { // @COMPAT the none schema is deprecated since 2.1.8 pcmk__schema_t *none_schema = NULL; entry = pcmk__get_schema(PCMK_VALUE_NONE); CRM_ASSERT((entry != NULL) && (entry->data != NULL)); none_schema = entry->data; if (!to_logs && (orig_version >= none_schema->schema_index)) { fprintf(stderr, "Schema validation of configuration is " "disabled (support for " PCMK_XA_VALIDATE_WITH " set to \"" PCMK_VALUE_NONE "\" is deprecated" " and will be removed in a future release)\n"); } } free(original_schema_name); return rc; } /*! * \internal * \brief Return a list of all schema files and any associated XSLT files * later than the given one * \brief Return a list of all schema versions later than the given one * * \param[in] schema The schema to compare against (for example, * "pacemaker-3.1.rng" or "pacemaker-3.1") * * \note The caller is responsible for freeing both the returned list and * the elements of the list */ GList * pcmk__schema_files_later_than(const char *name) { GList *lst = NULL; pcmk__schema_version_t ver; if (!version_from_filename(name, &ver)) { return lst; } for (GList *iter = g_list_nth(known_schemas, xml_latest_schema_index()); iter != NULL; iter = iter->prev) { pcmk__schema_t *schema = iter->data; if (schema_cmp(ver, schema->version) != -1) { continue; } for (GList *iter2 = g_list_last(schema->transforms); iter2 != NULL; iter2 = iter2->prev) { const struct dirent *entry = iter2->data; lst = g_list_prepend(lst, pcmk__str_copy(entry->d_name)); } lst = g_list_prepend(lst, crm_strdup_printf("%s.rng", schema->name)); } return lst; } static void append_href(xmlNode *xml, void *user_data) { GList **list = user_data; char *href = crm_element_value_copy(xml, "href"); if (href == NULL) { return; } *list = g_list_prepend(*list, href); } static void external_refs_in_schema(GList **list, const char *contents) { /* local-name()= is needed to ignore the xmlns= setting at the top of * the XML file. Otherwise, the xpath query will always return nothing. */ const char *search = "//*[local-name()='externalRef'] | //*[local-name()='include']"; xmlNode *xml = pcmk__xml_parse(contents); crm_foreach_xpath_result(xml, search, append_href, list); pcmk__xml_free(xml); } static int read_file_contents(const char *file, char **contents) { int rc = pcmk_rc_ok; char *path = NULL; if (pcmk__ends_with(file, ".rng")) { path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, file); } else { path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, file); } rc = pcmk__file_contents(path, contents); free(path); return rc; } static void add_schema_file_to_xml(xmlNode *parent, const char *file, GList **already_included) { char *contents = NULL; char *path = NULL; xmlNode *file_node = NULL; GList *includes = NULL; int rc = pcmk_rc_ok; /* If we already included this file, don't do so again. */ if (g_list_find_custom(*already_included, file, (GCompareFunc) strcmp) != NULL) { return; } /* Ensure whatever file we were given has a suffix we know about. If not, * just assume it's an RNG file. */ if (!pcmk__ends_with(file, ".rng") && !pcmk__ends_with(file, ".xsl")) { path = crm_strdup_printf("%s.rng", file); } else { path = pcmk__str_copy(file); } rc = read_file_contents(path, &contents); if (rc != pcmk_rc_ok || contents == NULL) { crm_warn("Could not read schema file %s: %s", file, pcmk_rc_str(rc)); free(path); return; } /* Create a new node with the contents of the file * as a CDATA block underneath it. */ file_node = pcmk__xe_create(parent, PCMK_XA_FILE); crm_xml_add(file_node, PCMK_XA_PATH, path); *already_included = g_list_prepend(*already_included, path); xmlAddChild(file_node, xmlNewCDataBlock(parent->doc, (pcmkXmlStr) contents, strlen(contents))); /* Scan the file for any or nodes and build up * a list of the files they reference. */ external_refs_in_schema(&includes, contents); /* For each referenced file, recurse to add it (and potentially anything it * references, ...) to the XML. */ for (GList *iter = includes; iter != NULL; iter = iter->next) { add_schema_file_to_xml(parent, iter->data, already_included); } free(contents); g_list_free_full(includes, free); } /*! * \internal * \brief Add an XML schema file and all the files it references as children * of a given XML node * * \param[in,out] parent The parent XML node * \param[in] name The schema version to compare against * (for example, "pacemaker-3.1" or "pacemaker-3.1.rng") * \param[in,out] already_included A list of names that have already been added * to the parent node. * * \note The caller is responsible for freeing both the returned list and * the elements of the list */ void pcmk__build_schema_xml_node(xmlNode *parent, const char *name, GList **already_included) { xmlNode *schema_node = pcmk__xe_create(parent, PCMK__XA_SCHEMA); crm_xml_add(schema_node, PCMK_XA_VERSION, name); add_schema_file_to_xml(schema_node, name, already_included); if (schema_node->children == NULL) { // Not needed if empty. May happen if name was invalid, for example. pcmk__xml_free(schema_node); } } /*! * \internal * \brief Return the directory containing any extra schema files that a * Pacemaker Remote node fetched from the cluster */ const char * pcmk__remote_schema_dir(void) { const char *dir = pcmk__env_option(PCMK__ENV_REMOTE_SCHEMA_DIRECTORY); if (pcmk__str_empty(dir)) { return PCMK__REMOTE_SCHEMA_DIR; } return dir; } /*! * \internal * \brief Warn if a given validation schema is deprecated * * \param[in] Schema name to check */ void pcmk__warn_if_schema_deprecated(const char *schema) { if (pcmk__str_eq(schema, PCMK_VALUE_NONE, pcmk__str_none|pcmk__str_null_matches)) { pcmk__config_warn("Support for " PCMK_XA_VALIDATE_WITH "='%s' is " "deprecated and will be removed in a future release " "without the possibility of upgrades (manually edit " "to use a supported schema)", pcmk__s(schema, "")); } } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START #include gboolean cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) { int rc = pcmk__update_configured_schema(xml, to_logs); if (best_version != NULL) { const char *name = crm_element_value(*xml, PCMK_XA_VALIDATE_WITH); if (name == NULL) { *best_version = -1; } else { GList *entry = pcmk__get_schema(name); pcmk__schema_t *schema = (entry == NULL)? NULL : entry->data; *best_version = (schema == NULL)? -1 : schema->schema_index; } } return (rc == pcmk_rc_ok)? TRUE: FALSE; } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/tests/xml/Makefile.am b/lib/common/tests/xml/Makefile.am index 7b11a31660..546e47e3de 100644 --- a/lib/common/tests/xml/Makefile.am +++ b/lib/common/tests/xml/Makefile.am @@ -1,27 +1,29 @@ # # Copyright 2022-2024 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # include $(top_srcdir)/mk/tap.mk include $(top_srcdir)/mk/unittest.mk # Add "_test" to the end of all test program names to simplify .gitignore. -check_PROGRAMS = pcmk__xe_copy_attrs_test \ +check_PROGRAMS = pcmk__xc_create_test \ + pcmk__xe_copy_attrs_test \ pcmk__xe_first_child_test \ pcmk__xe_foreach_child_test \ pcmk__xe_set_id_test \ pcmk__xe_set_score_test \ pcmk__xe_sort_attrs_test \ pcmk__xml_escape_test \ pcmk__xml_init_test \ pcmk__xml_is_name_char_test \ pcmk__xml_is_name_start_char_test \ pcmk__xml_needs_escape_test \ + pcmk__xml_new_doc_test \ pcmk__xml_sanitize_id_test TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/xml/pcmk__xc_create_test.c b/lib/common/tests/xml/pcmk__xc_create_test.c new file mode 100644 index 0000000000..4e25adc130 --- /dev/null +++ b/lib/common/tests/xml/pcmk__xc_create_test.c @@ -0,0 +1,77 @@ +/* + * Copyright 2024 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include "crmcommon_private.h" + +/* This tests new_private_data() indirectly for comment nodes. Testing + * free_private_data() would be much less straightforward and is not worth the + * hassle. + */ + +static void +assert_comment(xmlDoc *doc, const char *content) +{ + xmlNode *node = NULL; + xml_node_private_t *nodepriv = NULL; + xml_doc_private_t *docpriv = doc->_private; + + // Also clears existing doc flags + xml_track_changes((xmlNode *) doc, NULL, NULL, false); + + node = pcmk__xc_create(doc, content); + assert_non_null(node); + assert_int_equal(node->type, XML_COMMENT_NODE); + assert_ptr_equal(node->doc, doc); + + if (content == NULL) { + assert_null(node->content); + } else { + assert_non_null(node->content); + assert_string_equal((const char *) node->content, content); + } + + nodepriv = node->_private; + assert_non_null(nodepriv); + assert_int_equal(nodepriv->check, PCMK__XML_NODE_PRIVATE_MAGIC); + assert_true(pcmk_all_flags_set(nodepriv->flags, + pcmk__xf_dirty|pcmk__xf_created)); + + assert_true(pcmk_is_set(docpriv->flags, pcmk__xf_dirty)); + + pcmk__xml_free(node); +} + +static void +null_doc(void **state) +{ + pcmk__assert_asserts(pcmk__xc_create(NULL, NULL)); + pcmk__assert_asserts(pcmk__xc_create(NULL, "some content")); +} + +static void +with_doc(void **state) +{ + xmlDoc *doc = pcmk__xml_new_doc(); + + assert_non_null(doc); + assert_non_null(doc->_private); + + assert_comment(doc, NULL); + assert_comment(doc, "some content"); + + pcmk__xml_free_doc(doc); +} + +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, pcmk__xml_test_teardown_group, + cmocka_unit_test(null_doc), + cmocka_unit_test(with_doc)); diff --git a/lib/common/tests/xml/pcmk__xml_init_test.c b/lib/common/tests/xml/pcmk__xml_init_test.c index 1c65647f0a..301e64e2ad 100644 --- a/lib/common/tests/xml/pcmk__xml_init_test.c +++ b/lib/common/tests/xml/pcmk__xml_init_test.c @@ -1,230 +1,31 @@ /* * Copyright 2023-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include -#include #include -#include #include "crmcommon_private.h" -/* Copied from lib/common/xml.c */ -#define XML_DOC_PRIVATE_MAGIC 0x81726354UL -#define XML_NODE_PRIVATE_MAGIC 0x54637281UL - -static int -setup(void **state) { - pcmk__xml_init(); - return 0; -} - -static int -teardown(void **state) { - pcmk__xml_cleanup(); - return 0; -} - static void -buffer_scheme_test(void **state) { +buffer_scheme_test(void **state) +{ assert_int_equal(XML_BUFFER_ALLOC_DOUBLEIT, xmlGetBufferAllocationScheme()); } -/* These functions also serve as unit tests of the static new_private_data - * function. We can't test free_private_data because libxml will call that as - * part of freeing everything else. By the time we'd get back into a unit test - * where we could check that private members are NULL, the structure containing - * the private data would have been freed. - * - * This could probably be tested with a lot of function mocking, but that - * doesn't seem worth it. - */ - -static void -create_document_node(void **state) { - xml_doc_private_t *docpriv = NULL; - xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION); - - /* Double check things */ - assert_non_null(doc); - assert_int_equal(doc->type, XML_DOCUMENT_NODE); - - /* Check that the private data is initialized correctly */ - docpriv = doc->_private; - assert_non_null(docpriv); - assert_int_equal(docpriv->check, XML_DOC_PRIVATE_MAGIC); - assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty|pcmk__xf_created)); - - /* Clean up */ - xmlFreeDoc(doc); -} - static void -create_element_node(void **state) { - xml_doc_private_t *docpriv = NULL; - xml_node_private_t *priv = NULL; - xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION); - xmlNodePtr node = xmlNewDocNode(doc, NULL, (pcmkXmlStr) "test", NULL); - - /* Adding a node to the document marks it as dirty */ - docpriv = doc->_private; - assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty)); - - /* Double check things */ - assert_non_null(node); - assert_int_equal(node->type, XML_ELEMENT_NODE); - - /* Check that the private data is initialized correctly */ - priv = node->_private; - assert_non_null(priv); - assert_int_equal(priv->check, XML_NODE_PRIVATE_MAGIC); - assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created)); - - /* Clean up */ - xmlFreeNode(node); - xmlFreeDoc(doc); -} - -static void -create_attr_node(void **state) { - xml_doc_private_t *docpriv = NULL; - xml_node_private_t *priv = NULL; - xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION); - xmlNodePtr node = xmlNewDocNode(doc, NULL, (pcmkXmlStr) "test", NULL); - xmlAttrPtr attr = xmlNewProp(node, (pcmkXmlStr) PCMK_XA_NAME, - (pcmkXmlStr) "dummy-value"); - - /* Adding a node to the document marks it as dirty */ - docpriv = doc->_private; - assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty)); - - /* Double check things */ - assert_non_null(attr); - assert_int_equal(attr->type, XML_ATTRIBUTE_NODE); - - /* Check that the private data is initialized correctly */ - priv = attr->_private; - assert_non_null(priv); - assert_int_equal(priv->check, XML_NODE_PRIVATE_MAGIC); - assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created)); - - /* Clean up */ - xmlFreeNode(node); - xmlFreeDoc(doc); -} - -static void -create_comment_node(void **state) { - xml_doc_private_t *docpriv = NULL; - xml_node_private_t *priv = NULL; - xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION); - xmlNodePtr node = xmlNewDocComment(doc, (pcmkXmlStr) "blahblah"); - - /* Adding a node to the document marks it as dirty */ - docpriv = doc->_private; - assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty)); - - /* Double check things */ - assert_non_null(node); - assert_int_equal(node->type, XML_COMMENT_NODE); - - /* Check that the private data is initialized correctly */ - priv = node->_private; - assert_non_null(priv); - assert_int_equal(priv->check, XML_NODE_PRIVATE_MAGIC); - assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created)); - - /* Clean up */ - xmlFreeNode(node); - xmlFreeDoc(doc); -} - -static void -create_text_node(void **state) { - xml_doc_private_t *docpriv = NULL; - xml_node_private_t *priv = NULL; - xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION); - xmlNodePtr node = xmlNewDocText(doc, (pcmkXmlStr) "blahblah"); - - /* Adding a node to the document marks it as dirty */ - docpriv = doc->_private; - assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty)); - - /* Double check things */ - assert_non_null(node); - assert_int_equal(node->type, XML_TEXT_NODE); - - /* Check that no private data was created */ - priv = node->_private; - assert_null(priv); - - /* Clean up */ - xmlFreeNode(node); - xmlFreeDoc(doc); -} - -static void -create_dtd_node(void **state) { - xml_doc_private_t *docpriv = NULL; - xml_node_private_t *priv = NULL; - xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION); - xmlDtdPtr dtd = xmlNewDtd(doc, (pcmkXmlStr) PCMK_XA_NAME, - (pcmkXmlStr) "externalId", - (pcmkXmlStr) "systemId"); - - /* Adding a node to the document marks it as dirty */ - docpriv = doc->_private; - assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty)); - - /* Double check things */ - assert_non_null(dtd); - assert_int_equal(dtd->type, XML_DTD_NODE); - - /* Check that no private data was created */ - priv = dtd->_private; - assert_null(priv); - - /* Clean up */ - /* If you call xmlFreeDtd before xmlFreeDoc, you get a segfault */ - xmlFreeDoc(doc); -} - -static void -create_cdata_node(void **state) { - xml_doc_private_t *docpriv = NULL; - xml_node_private_t *priv = NULL; - xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION); - xmlNodePtr node = xmlNewCDataBlock(doc, (pcmkXmlStr) "blahblah", 8); - - /* Adding a node to the document marks it as dirty */ - docpriv = doc->_private; - assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty)); - - /* Double check things */ - assert_non_null(node); - assert_int_equal(node->type, XML_CDATA_SECTION_NODE); - - /* Check that no private data was created */ - priv = node->_private; - assert_null(priv); - - /* Clean up */ - xmlFreeNode(node); - xmlFreeDoc(doc); +schemas_initialized(void **state) +{ + assert_non_null(pcmk__find_x_0_schema()); } -PCMK__UNIT_TEST(setup, teardown, +// The group setup/teardown functions call pcmk__xml_init()/pcmk__cml_xleanup() +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, pcmk__xml_test_teardown_group, cmocka_unit_test(buffer_scheme_test), - cmocka_unit_test(create_document_node), - cmocka_unit_test(create_element_node), - cmocka_unit_test(create_attr_node), - cmocka_unit_test(create_comment_node), - cmocka_unit_test(create_text_node), - cmocka_unit_test(create_dtd_node), - cmocka_unit_test(create_cdata_node)); + cmocka_unit_test(schemas_initialized)) diff --git a/lib/common/tests/xml/pcmk__xml_new_doc_test.c b/lib/common/tests/xml/pcmk__xml_new_doc_test.c new file mode 100644 index 0000000000..762aec4007 --- /dev/null +++ b/lib/common/tests/xml/pcmk__xml_new_doc_test.c @@ -0,0 +1,39 @@ +/* + * Copyright 2024 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include "crmcommon_private.h" + +/* This tests new_private_data() indirectly for document nodes. Testing + * free_private_data() would be much less straightforward and is not worth the + * hassle. + */ + +static void +create_document_node(void **state) { + xml_doc_private_t *docpriv = NULL; + xmlDoc *doc = pcmk__xml_new_doc(); + + assert_non_null(doc); + assert_int_equal(doc->type, XML_DOCUMENT_NODE); + + docpriv = doc->_private; + assert_non_null(docpriv); + assert_int_equal(docpriv->check, PCMK__XML_DOC_PRIVATE_MAGIC); + assert_true(pcmk_all_flags_set(docpriv->flags, + pcmk__xf_dirty|pcmk__xf_created)); + + pcmk__xml_free_doc(doc); +} + +PCMK__UNIT_TEST(pcmk__xml_test_setup_group, pcmk__xml_test_teardown_group, + cmocka_unit_test(create_document_node)) diff --git a/lib/common/unittest.c b/lib/common/unittest.c index 98c306b40b..6bf4d94e97 100644 --- a/lib/common/unittest.c +++ b/lib/common/unittest.c @@ -1,165 +1,173 @@ /* * Copyright 2024 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 // LCOV_EXCL_START void pcmk__assert_validates(xmlNode *xml) { const char *schema_dir = NULL; char *cmd = NULL; gchar *out = NULL; gchar *err = NULL; gint status; GError *gerr = NULL; char *xmllint_input = crm_strdup_printf("%s/test-xmllint.XXXXXX", pcmk__get_tmpdir()); int fd; int rc; fd = mkstemp(xmllint_input); if (fd < 0) { fail_msg("Could not create temp file: %s", strerror(errno)); } rc = pcmk__xml2fd(fd, xml); if (rc != pcmk_rc_ok) { unlink(xmllint_input); fail_msg("Could not write temp file: %s", pcmk_rc_str(rc)); } close(fd); /* This should be set as part of AM_TESTS_ENVIRONMENT in Makefile.am. */ schema_dir = getenv("PCMK_schema_directory"); if (schema_dir == NULL) { unlink(xmllint_input); fail_msg("PCMK_schema_directory is not set in test environment"); } cmd = crm_strdup_printf("xmllint --relaxng %s/api/api-result.rng %s", schema_dir, xmllint_input); if (!g_spawn_command_line_sync(cmd, &out, &err, &status, &gerr)) { unlink(xmllint_input); fail_msg("Error occurred when performing validation: %s", gerr->message); } if (WIFEXITED(status) && WEXITSTATUS(status) != 0) { unlink(xmllint_input); fail_msg("XML validation failed: %s\n%s\n", out, err); } free(cmd); g_free(out); g_free(err); unlink(xmllint_input); free(xmllint_input); } +/*! + * \internal + * \brief Perform setup for a group of unit tests that manipulate XML + * + * This function is suitable for being passed as the first argument to the + * \c PCMK__UNIT_TEST macro. + * + * \param[in] state Ignored + * + * \return 0 + */ int pcmk__xml_test_setup_group(void **state) { - /* This needs to be run before we run unit tests that manipulate XML. - * Without it, document private data won't get created, which can cause - * segmentation faults or assertions in functions related to change - * tracking and ACLs. There's no harm in doing this before all tests. - */ + // Load schemas and set libxml2 buffer allocation scheme pcmk__xml_init(); return 0; } /*! * \internal - * \brief Perform teardown for a group of unit tests that manipulated XML + * \brief Perform teardown for a group of unit tests that manipulate XML * * This function is suitable for being passed as the second argument to the * \c PCMK__UNIT_TEST macro. * * \param[in] state Ignored * * \return 0 */ int pcmk__xml_test_teardown_group(void **state) { + // Clean up schemas and libxml2 global memory pcmk__xml_cleanup(); return 0; } char * pcmk__cib_test_copy_cib(const char *in_file) { char *in_path = crm_strdup_printf("%s/%s", getenv("PCMK_CTS_CLI_DIR"), in_file); char *out_path = NULL; char *contents = NULL; int fd; /* Copy the CIB over to a temp location so we can modify it. */ out_path = crm_strdup_printf("%s/test-cib.XXXXXX", pcmk__get_tmpdir()); fd = mkstemp(out_path); if (fd < 0) { free(out_path); return NULL; } if (pcmk__file_contents(in_path, &contents) != pcmk_rc_ok) { free(out_path); close(fd); return NULL; } if (pcmk__write_sync(fd, contents) != pcmk_rc_ok) { free(out_path); free(in_path); free(contents); close(fd); return NULL; } setenv("CIB_file", out_path, 1); return out_path; } void pcmk__cib_test_cleanup(char *out_path) { unlink(out_path); free(out_path); unsetenv("CIB_file"); } /*! * \internal * \brief Initialize logging for unit testing purposes * * \param[in] name What to use as system name for logging * \param[in] filename If not NULL, enable debug logs to this file (intended * for debugging during development rather than committed * unit tests) */ void pcmk__test_init_logging(const char *name, const char *filename) { pcmk__cli_init_logging(name, 0); if (filename != NULL) { pcmk__add_logfile(filename); set_crm_log_level(LOG_DEBUG); } } // LCOV_EXCL_STOP diff --git a/lib/common/xml.c b/lib/common/xml.c index 0ccca497eb..0432cb74bb 100644 --- a/lib/common/xml.c +++ b/lib/common/xml.c @@ -1,2606 +1,2579 @@ /* * Copyright 2004-2024 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 // uint32_t #include #include #include #include // stat(), S_ISREG, etc. #include #include // gboolean, GString #include #include #include // xmlGetUTF8Char() #include #include #include // PCMK__XML_LOG_BASE, etc. #include "crmcommon_private.h" +//! libxml2 supports only XML version 1.0, at least as of libxml2-2.12.5 +#define XML_VERSION ((pcmkXmlStr) "1.0") + /*! * \internal * \brief Apply a function to each XML node in a tree (pre-order, depth-first) * * \param[in,out] xml XML tree to traverse * \param[in,out] fn Function to call for each node (returns \c true to * continue traversing the tree or \c false to stop) * \param[in,out] user_data Argument to \p fn * * \return \c false if any \p fn call returned \c false, or \c true otherwise * * \note This function is recursive. */ bool pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *), void *user_data) { + if (xml == NULL) { + return true; + } + if (!fn(xml, user_data)) { return false; } for (xml = pcmk__xml_first_child(xml); xml != NULL; xml = pcmk__xml_next(xml)) { if (!pcmk__xml_tree_foreach(xml, fn, user_data)) { return false; } } return true; } bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy) { if(xml == NULL || xml->doc == NULL || xml->doc->_private == NULL) { return FALSE; } else if (!pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags, pcmk__xf_tracking)) { return FALSE; } else if (lazy && !pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags, pcmk__xf_lazy)) { return FALSE; } return TRUE; } -static inline void -set_parent_flag(xmlNode *xml, long flag) +void +pcmk__xml_set_parent_flags(xmlNode *xml, uint64_t flags) { - for(; xml; xml = xml->parent) { + for (; xml != NULL; xml = xml->parent) { xml_node_private_t *nodepriv = xml->_private; - if (nodepriv == NULL) { - /* During calls to xmlDocCopyNode(), _private will be unset for parent nodes */ - } else { - pcmk__set_xml_flags(nodepriv, flag); + if (nodepriv != NULL) { + pcmk__set_xml_flags(nodepriv, flags); } } } void pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag) { - if(xml && xml->doc && xml->doc->_private){ - /* During calls to xmlDocCopyNode(), xml->doc may be unset */ + if (xml != NULL) { xml_doc_private_t *docpriv = xml->doc->_private; pcmk__set_xml_flags(docpriv, flag); } } // Mark document, element, and all element's parents as changed void pcmk__mark_xml_node_dirty(xmlNode *xml) { pcmk__set_xml_doc_flag(xml, pcmk__xf_dirty); - set_parent_flag(xml, pcmk__xf_dirty); + pcmk__xml_set_parent_flags(xml, pcmk__xf_dirty); } /*! * \internal * \brief Clear flags on an XML node * * \param[in,out] xml XML node whose flags to reset * \param[in,out] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool reset_xml_node_flags(xmlNode *xml, void *user_data) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv != NULL) { nodepriv->flags = pcmk__xf_none; } return true; } /*! * \internal * \brief Set the \c pcmk__xf_dirty and \c pcmk__xf_created flags on an XML node * * \param[in,out] xml Node whose flags to set * \param[in] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool mark_xml_dirty_created(xmlNode *xml, void *user_data) { xml_node_private_t *nodepriv = xml->_private; if (nodepriv != NULL) { pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); } return true; } /*! * \internal * \brief Mark an XML tree as dirty and created, and mark its parents dirty * * Also mark the document dirty. * * \param[in,out] xml Tree to mark as dirty and created */ -void -pcmk__xml_mark_created(xmlNode *xml) +static void +mark_xml_tree_dirty_created(xmlNode *xml) { CRM_ASSERT(xml != NULL); if (!pcmk__tracking_xml_changes(xml, false)) { // Tracking is disabled for entire document return; } // Mark all parents and document dirty pcmk__mark_xml_node_dirty(xml); pcmk__xml_tree_foreach(xml, mark_xml_dirty_created, NULL); } -#define XML_DOC_PRIVATE_MAGIC 0x81726354UL -#define XML_NODE_PRIVATE_MAGIC 0x54637281UL - // Free an XML object previously marked as deleted static void free_deleted_object(void *data) { if(data) { pcmk__deleted_xml_t *deleted_obj = data; g_free(deleted_obj->path); free(deleted_obj); } } // Free and NULL user, ACLs, and deleted objects in an XML node's private data static void reset_xml_private_data(xml_doc_private_t *docpriv) { if (docpriv != NULL) { - CRM_ASSERT(docpriv->check == XML_DOC_PRIVATE_MAGIC); + CRM_ASSERT(docpriv->check == PCMK__XML_DOC_PRIVATE_MAGIC); free(docpriv->user); docpriv->user = NULL; if (docpriv->acls != NULL) { pcmk__free_acls(docpriv->acls); docpriv->acls = NULL; } if(docpriv->deleted_objs) { g_list_free_full(docpriv->deleted_objs, free_deleted_object); docpriv->deleted_objs = NULL; } } } -// Free all private data associated with an XML node -static void -free_private_data(xmlNode *node) -{ - /* Note: - - This function frees private data assosciated with an XML node, - unless the function is being called as a result of internal - XSLT cleanup. - - That could happen through, for example, the following chain of - function calls: - - xsltApplyStylesheetInternal - -> xsltFreeTransformContext - -> xsltFreeRVTs - -> xmlFreeDoc - - And in that case, the node would fulfill three conditions: - - 1. It would be a standalone document (i.e. it wouldn't be - part of a document) - 2. It would have a space-prefixed name (for reference, please - see xsltInternals.h: XSLT_MARK_RES_TREE_FRAG) - 3. It would carry its own payload in the _private field. - - We do not free data in this circumstance to avoid a failed - assertion on the XML_*_PRIVATE_MAGIC later. - - */ - if (node->name == NULL || node->name[0] != ' ') { - if (node->_private) { - if (node->type == XML_DOCUMENT_NODE) { - reset_xml_private_data(node->_private); - } else { - CRM_ASSERT(((xml_node_private_t *) node->_private)->check - == XML_NODE_PRIVATE_MAGIC); - /* nothing dynamically allocated nested */ - } - free(node->_private); - node->_private = NULL; - } +/*! + * \internal + * \brief Allocate and initialize private data for an XML node + * + * \param[in,out] node XML node whose private data to initialize + * \param[in] user_data Ignored + * + * \return \c true (to continue traversing the tree) + * + * \note This is compatible with \c pcmk__xml_tree_foreach(). + */ +static bool +new_private_data(xmlNode *node, void *user_data) +{ + CRM_CHECK(node != NULL, return true); + + if (node->_private != NULL) { + return true; } -} -// Allocate and initialize private data for an XML node -static void -new_private_data(xmlNode *node) -{ switch (node->type) { - case XML_DOCUMENT_NODE: { - xml_doc_private_t *docpriv = - pcmk__assert_alloc(1, sizeof(xml_doc_private_t)); - - docpriv->check = XML_DOC_PRIVATE_MAGIC; - /* Flags will be reset if necessary when tracking is enabled */ - pcmk__set_xml_flags(docpriv, pcmk__xf_dirty|pcmk__xf_created); - node->_private = docpriv; + case XML_DOCUMENT_NODE: + { + xml_doc_private_t *docpriv = + pcmk__assert_alloc(1, sizeof(xml_doc_private_t)); + + docpriv->check = PCMK__XML_DOC_PRIVATE_MAGIC; + node->_private = docpriv; + pcmk__set_xml_flags(docpriv, pcmk__xf_dirty|pcmk__xf_created); + } break; - } + case XML_ELEMENT_NODE: case XML_ATTRIBUTE_NODE: - case XML_COMMENT_NODE: { - xml_node_private_t *nodepriv = - pcmk__assert_alloc(1, sizeof(xml_node_private_t)); - - nodepriv->check = XML_NODE_PRIVATE_MAGIC; - /* Flags will be reset if necessary when tracking is enabled */ - pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); - node->_private = nodepriv; - if (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__mark_xml_node_dirty(node); + case XML_COMMENT_NODE: + { + xml_node_private_t *nodepriv = + pcmk__assert_alloc(1, sizeof(xml_node_private_t)); + + nodepriv->check = PCMK__XML_NODE_PRIVATE_MAGIC; + node->_private = nodepriv; + pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); + + for (xmlAttr *iter = pcmk__xe_first_attr(node); iter != NULL; + iter = iter->next) { + + new_private_data((xmlNode *) iter, user_data); + } } break; - } + case XML_TEXT_NODE: case XML_DTD_NODE: case XML_CDATA_SECTION_NODE: - break; + return true; + default: - /* Ignore */ - crm_trace("Ignoring %p %d", node, node->type); CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE); - break; + return true; + } + + if (pcmk__tracking_xml_changes(node, false)) { + pcmk__mark_xml_node_dirty(node); + } + return true; +} + +/*! + * \internal + * \brief Free private data for an XML node + * + * \param[in,out] node XML node whose private data to free + * \param[in] user_data Ignored + * + * \return \c true (to continue traversing the tree) + * + * \note This is compatible with \c pcmk__xml_tree_foreach(). + */ +static bool +free_private_data(xmlNode *node, void *user_data) +{ + CRM_CHECK(node != NULL, return true); + + if (node->_private == NULL) { + return true; } + + if (node->type == XML_DOCUMENT_NODE) { + reset_xml_private_data((xml_doc_private_t *) node->_private); + + } else { + xml_node_private_t *nodepriv = node->_private; + + CRM_ASSERT(nodepriv->check == PCMK__XML_NODE_PRIVATE_MAGIC); + + for (xmlAttr *iter = pcmk__xe_first_attr(node); iter != NULL; + iter = iter->next) { + + free_private_data((xmlNode *) iter, user_data); + } + } + free(node->_private); + node->_private = NULL; + return true; +} + +/*! + * \internal + * \brief Allocate and initialize private data recursively for an XML tree + * + * \param[in,out] node XML node whose private data to initialize + */ +void +pcmk__xml_new_private_data(xmlNode *xml) +{ + pcmk__xml_tree_foreach(xml, new_private_data, NULL); +} + +/*! + * \internal + * \brief Free private data recursively for an XML tree + * + * \param[in,out] node XML node whose private data to free + */ +void +pcmk__xml_free_private_data(xmlNode *xml) +{ + pcmk__xml_tree_foreach(xml, free_private_data, NULL); } 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_doc_flag(xml, pcmk__xf_tracking); if(enforce_acls) { if(acl_source == NULL) { acl_source = xml; } pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_enabled); pcmk__unpack_acl(acl_source, xml, user); pcmk__apply_acl(xml); } } bool xml_tracking_changes(xmlNode * xml) { return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL) && pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags, pcmk__xf_tracking); } bool xml_document_dirty(xmlNode *xml) { return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL) && pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags, pcmk__xf_dirty); } /*! * \internal * \brief Return ordinal position of an XML node among its siblings * * \param[in] xml XML node to check * \param[in] ignore_if_set Don't count siblings with this flag set * * \return Ordinal position of \p xml (starting with 0) */ int pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set) { int position = 0; for (const xmlNode *cIter = xml; cIter->prev; cIter = cIter->prev) { xml_node_private_t *nodepriv = ((xmlNode*)cIter->prev)->_private; if (!pcmk_is_set(nodepriv->flags, ignore_if_set)) { position++; } } return position; } /*! * \internal * \brief Remove all attributes marked as deleted from an XML node * * \param[in,out] xml XML node whose deleted attributes to remove * \param[in,out] user_data Ignored * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool accept_attr_deletions(xmlNode *xml, void *user_data) { reset_xml_node_flags(xml, NULL); pcmk__xe_remove_matching_attrs(xml, pcmk__marked_as_deleted, NULL); return true; } /*! * \internal * \brief Find first child XML node matching another given XML node * * \param[in] haystack XML whose children should be checked * \param[in] needle XML to match (comment content or element name and ID) * \param[in] exact If true and needle is a comment, position must match */ xmlNode * pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact) { CRM_CHECK(needle != NULL, return NULL); if (needle->type == XML_COMMENT_NODE) { return pcmk__xc_match(haystack, needle, exact); } else { const char *id = pcmk__xe_id(needle); const char *attr = (id == NULL)? NULL : PCMK_XA_ID; return pcmk__xe_first_child(haystack, (const char *) needle->name, attr, id); } } void xml_accept_changes(xmlNode * xml) { xmlNode *top = NULL; xml_doc_private_t *docpriv = NULL; if(xml == NULL) { return; } crm_trace("Accepting changes to %p", xml); docpriv = xml->doc->_private; top = xmlDocGetRootElement(xml->doc); reset_xml_private_data(xml->doc->_private); if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) { docpriv->flags = pcmk__xf_none; return; } docpriv->flags = pcmk__xf_none; pcmk__xml_tree_foreach(top, accept_attr_deletions, NULL); } /*! * \internal * \brief Find first XML child element matching given criteria * * \param[in] parent XML element to search (can be \c NULL) * \param[in] node_name If not \c NULL, only match children of this type * \param[in] attr_n If not \c NULL, only match children with an attribute * of this name. * \param[in] attr_v If \p attr_n and this are not NULL, only match children * with an attribute named \p attr_n and this value * * \return Matching XML child element, or \c NULL if none found */ xmlNode * pcmk__xe_first_child(const xmlNode *parent, const char *node_name, const char *attr_n, const char *attr_v) { xmlNode *child = NULL; const char *parent_name = ""; CRM_CHECK((attr_v == NULL) || (attr_n != NULL), return NULL); if (parent != NULL) { child = parent->children; while ((child != NULL) && (child->type != XML_ELEMENT_NODE)) { child = child->next; } parent_name = (const char *) parent->name; } for (; child != NULL; child = pcmk__xe_next(child)) { const char *value = NULL; if ((node_name != NULL) && !pcmk__xe_is(child, node_name)) { // Node name mismatch continue; } if (attr_n == NULL) { // No attribute match needed return child; } value = crm_element_value(child, attr_n); if ((attr_v == NULL) && (value != NULL)) { // attr_v == NULL: Attribute attr_n must be set (to any value) return child; } if ((attr_v != NULL) && (pcmk__str_eq(value, attr_v, pcmk__str_none))) { // attr_v != NULL: Attribute attr_n must be set to value attr_v return child; } } if (node_name == NULL) { node_name = "(any)"; // For logging } if (attr_n != NULL) { crm_trace("XML child node <%s %s=%s> not found in %s", node_name, attr_n, attr_v, parent_name); } else { crm_trace("XML child node <%s> not found in %s", node_name, parent_name); } return NULL; } /*! * \internal * \brief Set an XML attribute, expanding \c ++ and \c += where appropriate * * If \p target already has an attribute named \p name set to an integer value * and \p value is an addition assignment expression on \p name, then expand * \p value to an integer and set attribute \p name to the expanded value in * \p target. * * Otherwise, set attribute \p name on \p target using the literal \p value. * * The original attribute value in \p target and the number in an assignment * expression in \p value are parsed and added as scores (that is, their values * are capped at \c INFINITY and \c -INFINITY). For more details, refer to * \c char2score(). * * For example, suppose \p target has an attribute named \c "X" with value * \c "5", and that \p name is \c "X". * * If \p value is \c "X++", the new value of \c "X" in \p target is \c "6". * * If \p value is \c "X+=3", the new value of \c "X" in \p target is \c "8". * * If \p value is \c "val", the new value of \c "X" in \p target is \c "val". * * If \p value is \c "Y++", the new value of \c "X" in \p target is \c "Y++". * * \param[in,out] target XML node whose attribute to set * \param[in] name Name of the attribute to set * \param[in] value New value of attribute to set * * \return Standard Pacemaker return code (specifically, \c EINVAL on invalid * argument, or \c pcmk_rc_ok otherwise) */ int pcmk__xe_set_score(xmlNode *target, const char *name, const char *value) { const char *old_value = NULL; CRM_CHECK((target != NULL) && (name != NULL), return EINVAL); if (value == NULL) { return pcmk_rc_ok; } old_value = crm_element_value(target, name); // If no previous value, skip to default case and set the value unexpanded. if (old_value != NULL) { const char *n = name; const char *v = value; // Stop at first character that differs between name and value for (; (*n == *v) && (*n != '\0'); n++, v++); // If value begins with name followed by a "++" or "+=" if ((*n == '\0') && (*v++ == '+') && ((*v == '+') || (*v == '='))) { // If we're expanding ourselves, no previous value was set; use 0 int old_value_i = (old_value != value)? char2score(old_value) : 0; /* value="X++": new value of X is old_value + 1 * value="X+=Y": new value of X is old_value + Y (for some number Y) */ int add = (*v == '+')? 1 : char2score(++v); crm_xml_add_int(target, name, pcmk__add_scores(old_value_i, add)); return pcmk_rc_ok; } } // Default case: set the attribute unexpanded (with value treated literally) if (old_value != value) { crm_xml_add(target, name, value); } return pcmk_rc_ok; } /*! * \internal * \brief Copy XML attributes from a source element to a target element * * This is similar to \c xmlCopyPropList() except that attributes are marked * as dirty for change tracking purposes. * * \param[in,out] target XML element to receive copied attributes from \p src * \param[in] src XML element whose attributes to copy to \p target * \param[in] flags Group of enum pcmk__xa_flags * * \return Standard Pacemaker return code */ int pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags) { CRM_CHECK((src != NULL) && (target != NULL), return EINVAL); for (xmlAttr *attr = pcmk__xe_first_attr(src); attr != NULL; attr = attr->next) { const char *name = (const char *) attr->name; const char *value = pcmk__xml_attr_value(attr); if (pcmk_is_set(flags, pcmk__xaf_no_overwrite) && (crm_element_value(target, name) != NULL)) { continue; } if (pcmk_is_set(flags, pcmk__xaf_score_update)) { pcmk__xe_set_score(target, name, value); } else { crm_xml_add(target, name, value); } } return pcmk_rc_ok; } /*! * \internal - * \brief Move an XML attribute to the end of its element's attribute list + * \brief Compare two XML attributes by name * - * This does not consider ACLs and does not mark the attribute as deleted or - * dirty. Upon return, the attribute still exists and is set to the same value - * as before the call. Only its position in the attribute list may change. + * \param[in] a First XML attribute to compare + * \param[in] b Second XML attribute to compare * - * \param[in] data \c pcmk_nvpair_t representing an XML attribute - * \param[in,out] user_data XML element whose attribute to move + * \retval negative \c a->name is \c NULL or comes before \c b->name + * lexicographically + * \retval 0 \c a->name and \c b->name are equal + * \retval positive \c b->name is \c NULL or comes before \c a->name + * lexicographically */ -static void -move_xml_attr_to_end(gpointer data, gpointer user_data) +static gint +compare_xml_attr(gconstpointer a, gconstpointer b) { - const pcmk_nvpair_t *pair = data; - xmlNode *xml = user_data; + const xmlAttr *attr_a = a; + const xmlAttr *attr_b = b; - xmlAttr *attr = xmlHasProp(xml, (pcmkXmlStr) pair->name); - xml_node_private_t *nodepriv = attr->_private; - uint32_t flags = (nodepriv != NULL)? nodepriv->flags : pcmk__xf_none; - - xmlRemoveProp(attr); - attr = xmlSetProp(xml, (pcmkXmlStr) pair->name, (pcmkXmlStr) pair->value); - - nodepriv = attr->_private; - if (nodepriv != NULL) { - nodepriv->flags = flags; - } + return pcmk__strcmp((const char *) attr_a->name, + (const char *) attr_b->name, pcmk__str_none); } /*! * \internal * \brief Sort an XML element's attributes by name * * This does not consider ACLs and does not mark the attributes as deleted or * dirty. Upon return, all attributes still exist and are set to the same values * as before the call. The only thing that may change is the order of the * attribute list. * * \param[in,out] xml XML element whose attributes to sort */ void pcmk__xe_sort_attrs(xmlNode *xml) { - xmlAttr *attr = pcmk__xe_first_attr(xml); - GSList *nvpairs = NULL; - - if ((attr == NULL) || (attr->next == NULL)) { - return; - } - - nvpairs = pcmk_xml_attrs2nvpairs(xml); - nvpairs = pcmk_sort_nvpairs(nvpairs); - - // Reset attributes in sorted order - g_slist_foreach(nvpairs, move_xml_attr_to_end, xml); + GSList *attr_list = NULL; - pcmk_free_nvpairs(nvpairs); -} - -/*! - * \internal - * \brief Remove an XML attribute from an element - * - * \param[in,out] element XML element that owns \p attr - * \param[in,out] attr XML attribute to remove from \p element - * - * \return Standard Pacemaker return code (\c EPERM if ACLs prevent removal of - * attributes from \p element, or \c pcmk_rc_ok otherwise) - */ -static int -remove_xe_attr(xmlNode *element, xmlAttr *attr) -{ - if (attr == NULL) { - return pcmk_rc_ok; + for (xmlAttr *iter = pcmk__xe_first_attr(xml); iter != NULL; + iter = iter->next) { + attr_list = g_slist_prepend(attr_list, iter); } + attr_list = g_slist_sort(attr_list, compare_xml_attr); - if (!pcmk__check_acl(element, NULL, pcmk__xf_acl_write)) { - // ACLs apply to element, not to particular attributes - crm_trace("ACLs prevent removal of attributes from %s element", - (const char *) element->name); - return EPERM; - } + for (GSList *iter = attr_list; iter != NULL; iter = iter->next) { + xmlNode *attr = iter->data; - if (pcmk__tracking_xml_changes(element, false)) { - // Leave in place (marked for removal) until after diff is calculated - set_parent_flag(element, pcmk__xf_dirty); - pcmk__set_xml_flags((xml_node_private_t *) attr->_private, - pcmk__xf_deleted); - } else { - xmlRemoveProp(attr); + xmlUnlinkNode(attr); + xmlAddChild(xml, attr); } - return pcmk_rc_ok; + g_slist_free(attr_list); } /*! * \internal * \brief Remove a named attribute from an XML element * * \param[in,out] element XML element to remove an attribute from * \param[in] name Name of attribute to remove */ void pcmk__xe_remove_attr(xmlNode *element, const char *name) { if (name != NULL) { - remove_xe_attr(element, xmlHasProp(element, (pcmkXmlStr) name)); + pcmk__xa_remove(xmlHasProp(element, (pcmkXmlStr) name), false); } } /*! * \internal * \brief Remove a named attribute from an XML element * * This is a wrapper for \c pcmk__xe_remove_attr() for use with * \c pcmk__xml_tree_foreach(). * * \param[in,out] xml XML element to remove an attribute from * \param[in] user_data Name of attribute to remove * * \return \c true (to continue traversing the tree) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ bool pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data) { const char *name = user_data; pcmk__xe_remove_attr(xml, name); return true; } /*! * \internal * \brief Remove an XML element's attributes that match some criteria * * \param[in,out] element XML element to modify * \param[in] match If not NULL, only remove attributes for which * this function returns true * \param[in,out] user_data Data to pass to \p match */ void pcmk__xe_remove_matching_attrs(xmlNode *element, bool (*match)(xmlAttrPtr, void *), void *user_data) { xmlAttrPtr next = NULL; for (xmlAttrPtr a = pcmk__xe_first_attr(element); a != NULL; a = next) { next = a->next; // Grab now because attribute might get removed if ((match == NULL) || match(a, user_data)) { - if (remove_xe_attr(element, a) != pcmk_rc_ok) { + if (pcmk__xa_remove(a, false) != pcmk_rc_ok) { return; } } } } /*! * \internal * \brief Create a new XML element under a given parent * * \param[in,out] parent XML element that will be the new element's parent * (\c NULL to create a new XML document with the new * node as root) * \param[in] name Name of new element * * \return Newly created XML element (guaranteed not to be \c NULL) */ xmlNode * pcmk__xe_create(xmlNode *parent, const char *name) { xmlNode *node = NULL; CRM_ASSERT(!pcmk__str_empty(name)); if (parent == NULL) { - xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION); - - pcmk__mem_assert(doc); + xmlDoc *doc = pcmk__xml_new_doc(); node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL); pcmk__mem_assert(node); xmlDocSetRootElement(doc, node); } else { node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL); pcmk__mem_assert(node); } - pcmk__xml_mark_created(node); + pcmk__xml_new_private_data(node); return node; } +/*! + * \internal + * \brief Create a new XML document + * + * \return Newly allocated XML document (guaranteed not to be \c NULL) + * + * \note The caller is responsible for freeing the return value using + * \c pcmk__xml_free_doc(). + */ +xmlDoc * +pcmk__xml_new_doc(void) +{ + xmlDoc *doc = xmlNewDoc(XML_VERSION); + + pcmk__mem_assert(doc); + pcmk__xml_new_private_data((xmlNode *) doc); + return doc; +} + +/*! + * \internal + * \brief Free a new XML document + * + * \param[in,out] doc XML document to free + */ +void +pcmk__xml_free_doc(xmlDoc *doc) +{ + if (doc != NULL) { + pcmk__xml_free_private_data((xmlNode *) doc); + xmlFreeDoc(doc); + } +} + /*! * \internal * \brief Set a formatted string as an XML node's content * * \param[in,out] node Node whose content to set * \param[in] format printf(3)-style format string * \param[in] ... Arguments for \p format * * \note This function escapes special characters. \c xmlNodeSetContent() does * not. */ G_GNUC_PRINTF(2, 3) void pcmk__xe_set_content(xmlNode *node, const char *format, ...) { if (node != NULL) { const char *content = NULL; char *buf = NULL; + /* xmlNodeSetContent() frees node->children and replaces it with new + * text. If this function is called for a node that already has a non- + * text child, it's a bug. + */ + CRM_CHECK((node->children == NULL) + || (node->children->type == XML_TEXT_NODE), + return); + if (strchr(format, '%') == NULL) { // Nothing to format content = format; } else { va_list ap; va_start(ap, format); if (pcmk__str_eq(format, "%s", pcmk__str_none)) { // No need to make a copy content = va_arg(ap, const char *); } else { CRM_ASSERT(vasprintf(&buf, format, ap) >= 0); content = buf; } va_end(ap); } xmlNodeSetContent(node, (pcmkXmlStr) content); free(buf); } } /*! * \internal * \brief Check whether the first character of a string is an XML NameStartChar * * See https://www.w3.org/TR/xml/#NT-NameStartChar. * * This is almost identical to libxml2's \c xmlIsDocNameStartChar(), but they * don't expose it as part of the public API. * * \param[in] utf8 UTF-8 encoded string * \param[out] len If not \c NULL, where to store size in bytes of first * character in \p utf8 * * \return \c true if \p utf8 begins with a valid XML NameStartChar, or \c false * otherwise */ bool pcmk__xml_is_name_start_char(const char *utf8, int *len) { int c = 0; int local_len = 0; if (len == NULL) { len = &local_len; } /* xmlGetUTF8Char() abuses the len argument. At call time, it must be set to * "the minimum number of bytes present in the sequence... to assure the * next character is completely contained within the sequence." It's similar * to the "n" in the strn*() functions. However, this doesn't make any sense * for null-terminated strings, and there's no value that indicates "keep * going until '\0'." So we set it to 4, the max number of bytes in a UTF-8 * character. * * At return, it's set to the actual number of bytes in the char, or 0 on * error. */ *len = 4; // Note: xmlGetUTF8Char() assumes a 32-bit int c = xmlGetUTF8Char((pcmkXmlStr) utf8, len); if (c < 0) { GString *buf = g_string_sized_new(32); for (int i = 0; (i < 4) && (utf8[i] != '\0'); i++) { g_string_append_printf(buf, " 0x%.2X", utf8[i]); } crm_info("Invalid UTF-8 character (bytes:%s)", (pcmk__str_empty(buf->str)? " " : buf->str)); g_string_free(buf, TRUE); return false; } return (c == '_') || (c == ':') || ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')) || ((c >= 0xC0) && (c <= 0xD6)) || ((c >= 0xD8) && (c <= 0xF6)) || ((c >= 0xF8) && (c <= 0x2FF)) || ((c >= 0x370) && (c <= 0x37D)) || ((c >= 0x37F) && (c <= 0x1FFF)) || ((c >= 0x200C) && (c <= 0x200D)) || ((c >= 0x2070) && (c <= 0x218F)) || ((c >= 0x2C00) && (c <= 0x2FEF)) || ((c >= 0x3001) && (c <= 0xD7FF)) || ((c >= 0xF900) && (c <= 0xFDCF)) || ((c >= 0xFDF0) && (c <= 0xFFFD)) || ((c >= 0x10000) && (c <= 0xEFFFF)); } /*! * \internal * \brief Check whether the first character of a string is an XML NameChar * * See https://www.w3.org/TR/xml/#NT-NameChar. * * This is almost identical to libxml2's \c xmlIsDocNameChar(), but they don't * expose it as part of the public API. * * \param[in] utf8 UTF-8 encoded string * \param[out] len If not \c NULL, where to store size in bytes of first * character in \p utf8 * * \return \c true if \p utf8 begins with a valid XML NameChar, or \c false * otherwise */ bool pcmk__xml_is_name_char(const char *utf8, int *len) { int c = 0; int local_len = 0; if (len == NULL) { len = &local_len; } // See comment regarding len in pcmk__xml_is_name_start_char() *len = 4; // Note: xmlGetUTF8Char() assumes a 32-bit int c = xmlGetUTF8Char((pcmkXmlStr) utf8, len); if (c < 0) { GString *buf = g_string_sized_new(32); for (int i = 0; (i < 4) && (utf8[i] != '\0'); i++) { g_string_append_printf(buf, " 0x%.2X", utf8[i]); } crm_info("Invalid UTF-8 character (bytes:%s)", (pcmk__str_empty(buf->str)? " " : buf->str)); g_string_free(buf, TRUE); return false; } return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')) || ((c >= '0') && (c <= '9')) || (c == '_') || (c == ':') || (c == '-') || (c == '.') || (c == 0xB7) || ((c >= 0xC0) && (c <= 0xD6)) || ((c >= 0xD8) && (c <= 0xF6)) || ((c >= 0xF8) && (c <= 0x2FF)) || ((c >= 0x300) && (c <= 0x36F)) || ((c >= 0x370) && (c <= 0x37D)) || ((c >= 0x37F) && (c <= 0x1FFF)) || ((c >= 0x200C) && (c <= 0x200D)) || ((c >= 0x203F) && (c <= 0x2040)) || ((c >= 0x2070) && (c <= 0x218F)) || ((c >= 0x2C00) && (c <= 0x2FEF)) || ((c >= 0x3001) && (c <= 0xD7FF)) || ((c >= 0xF900) && (c <= 0xFDCF)) || ((c >= 0xFDF0) && (c <= 0xFFFD)) || ((c >= 0x10000) && (c <= 0xEFFFF)); } /*! * \internal * \brief Sanitize a string so it is usable as an XML ID * * An ID must match the Name production as defined here: * https://www.w3.org/TR/xml/#NT-Name. * * Convert an invalid start character to \c '_'. Convert an invalid character * after the start character to \c '.'. * * \param[in,out] id String to sanitize */ void pcmk__xml_sanitize_id(char *id) { bool valid = true; int len = 0; // If id is empty or NULL, there's no way to make it a valid XML ID CRM_ASSERT(!pcmk__str_empty(id)); /* @TODO Suppose there are two strings and each has an invalid ID character * in the same position. The strings are otherwise identical. Both strings * will be sanitized to the same valid ID, which is incorrect. * * The caller is responsible for ensuring the sanitized ID does not already * exist in a given XML document before using it, if uniqueness is desired. */ valid = pcmk__xml_is_name_start_char(id, &len); CRM_CHECK(len > 0, return); // UTF-8 encoding error if (!valid) { *id = '_'; for (int i = 1; i < len; i++) { id[i] = '.'; } } for (id += len; *id != '\0'; id += len) { valid = pcmk__xml_is_name_char(id, &len); CRM_CHECK(len > 0, return); // UTF-8 encoding error if (!valid) { for (int i = 0; i < len; i++) { id[i] = '.'; } } } } /*! * \internal * \brief Set a formatted string as an XML element's ID * * If the formatted string would not be a valid ID, it's first sanitized by * \c pcmk__xml_sanitize_id(). * * \param[in,out] node Node whose ID to set * \param[in] format printf(3)-style format string * \param[in] ... Arguments for \p format */ G_GNUC_PRINTF(2, 3) void pcmk__xe_set_id(xmlNode *node, const char *format, ...) { char *id = NULL; va_list ap; CRM_ASSERT(!pcmk__str_empty(format)); if (node == NULL) { return; } va_start(ap, format); CRM_ASSERT(vasprintf(&id, format, ap) >= 0); va_end(ap); if (!xmlValidateNameValue((pcmkXmlStr) id)) { pcmk__xml_sanitize_id(id); } crm_xml_add(node, PCMK_XA_ID, id); free(id); } +/*! + * \internal + * \brief Free an XML tree without ACL checks or change tracking + * + * \param[in,out] xml XML node to free + */ +static void +free_xml_node(xmlNode *xml) +{ + pcmk__xml_free_private_data(xml); + xmlUnlinkNode(xml); + xmlFreeNode(xml); +} + /*! * \internal * \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled * * If \p node is the root of its document, free the entire document. * * \param[in,out] node XML node to free * \param[in] position Position of \p node among its siblings for change * tracking (negative to calculate automatically if * needed) */ static void free_xml_with_position(xmlNode *node, int position) { xmlDoc *doc = NULL; xml_node_private_t *nodepriv = NULL; if (node == NULL) { return; } doc = node->doc; nodepriv = node->_private; if ((doc != NULL) && (xmlDocGetRootElement(doc) == node)) { /* @TODO Should we check ACLs first? Otherwise it seems like we could * free the root element without write permission. */ - xmlFreeDoc(doc); + pcmk__xml_free_doc(doc); return; } if (!pcmk__check_acl(node, NULL, pcmk__xf_acl_write)) { GString *xpath = NULL; pcmk__if_tracing({}, return); xpath = pcmk__element_xpath(node); qb_log_from_external_source(__func__, __FILE__, "Cannot remove %s %x", LOG_TRACE, __LINE__, 0, xpath->str, nodepriv->flags); g_string_free(xpath, TRUE); return; } if ((doc != NULL) && pcmk__tracking_xml_changes(node, false) && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { xml_doc_private_t *docpriv = doc->_private; GString *xpath = pcmk__element_xpath(node); if (xpath != NULL) { pcmk__deleted_xml_t *deleted_obj = NULL; crm_trace("Deleting %s %p from %p", xpath->str, node, doc); deleted_obj = pcmk__assert_alloc(1, sizeof(pcmk__deleted_xml_t)); deleted_obj->path = g_string_free(xpath, FALSE); deleted_obj->position = -1; // Record the position only for XML comments for now if (node->type == XML_COMMENT_NODE) { if (position >= 0) { deleted_obj->position = position; } else { deleted_obj->position = pcmk__xml_position(node, pcmk__xf_skip); } } docpriv->deleted_objs = g_list_append(docpriv->deleted_objs, deleted_obj); pcmk__set_xml_doc_flag(node, pcmk__xf_dirty); } } - xmlUnlinkNode(node); - xmlFreeNode(node); + free_xml_node(node); } /*! * \internal * \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled * * If \p xml is the root of its document, free the entire document. * * \param[in,out] xml XML node to free */ void pcmk__xml_free(xmlNode *xml) { free_xml_with_position(xml, -1); } /*! * \internal * \brief Make a deep copy of an XML node under a given parent * * \param[in,out] parent XML element that will be the copy's parent (\c NULL * to create a new XML document with the copy as root) * \param[in] src XML node to copy * * \return Deep copy of \p src, or \c NULL if \p src is \c NULL */ xmlNode * pcmk__xml_copy(xmlNode *parent, xmlNode *src) { xmlNode *copy = NULL; if (src == NULL) { return NULL; } if (parent == NULL) { xmlDoc *doc = NULL; // The copy will be the root element of a new document CRM_ASSERT(src->type == XML_ELEMENT_NODE); - doc = xmlNewDoc(PCMK__XML_VERSION); - pcmk__mem_assert(doc); - + doc = pcmk__xml_new_doc(); copy = xmlDocCopyNode(src, doc, 1); pcmk__mem_assert(copy); xmlDocSetRootElement(doc, copy); } else { copy = xmlDocCopyNode(src, parent->doc, 1); pcmk__mem_assert(copy); xmlAddChild(parent, copy); } - pcmk__xml_mark_created(copy); + pcmk__xml_new_private_data(copy); return copy; } /*! * \internal * \brief Remove XML text nodes from specified XML and all its children * * \param[in,out] xml XML to strip text from */ void pcmk__strip_xml_text(xmlNode *xml) { xmlNode *iter = xml->children; while (iter) { xmlNode *next = iter->next; switch (iter->type) { case XML_TEXT_NODE: - xmlUnlinkNode(iter); - xmlFreeNode(iter); + free_xml_node(iter); break; case XML_ELEMENT_NODE: /* Search it */ pcmk__strip_xml_text(iter); break; default: /* Leave it */ break; } iter = next; } } /*! * \internal * \brief Add a "last written" attribute to an XML element, set to current time * * \param[in,out] xe XML element to add attribute to * * \return Value that was set, or NULL on error */ const char * pcmk__xe_add_last_written(xmlNode *xe) { char *now_s = pcmk__epoch2str(NULL, 0); const char *result = NULL; result = crm_xml_add(xe, PCMK_XA_CIB_LAST_WRITTEN, pcmk__s(now_s, "Could not determine current time")); free(now_s); return result; } /*! * \internal * \brief Check whether a string has XML special characters that must be escaped * * See \c pcmk__xml_escape() and \c pcmk__xml_escape_type for more details. * * \param[in] text String to check * \param[in] type Type of escaping * * \return \c true if \p text has special characters that need to be escaped, or * \c false otherwise */ bool pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type) { if (text == NULL) { return false; } while (*text != '\0') { switch (type) { case pcmk__xml_escape_text: switch (*text) { case '<': case '>': case '&': return true; case '\n': case '\t': break; default: if (g_ascii_iscntrl(*text)) { return true; } break; } break; case pcmk__xml_escape_attr: switch (*text) { case '<': case '>': case '&': case '"': return true; default: if (g_ascii_iscntrl(*text)) { return true; } break; } break; case pcmk__xml_escape_attr_pretty: switch (*text) { case '\n': case '\r': case '\t': case '"': return true; default: break; } break; default: // Invalid enum value CRM_ASSERT(false); break; } text = g_utf8_next_char(text); } return false; } /*! * \internal * \brief Replace special characters with their XML escape sequences * * \param[in] text Text to escape * \param[in] type Type of escaping * * \return Newly allocated string equivalent to \p text but with special * characters replaced with XML escape sequences (or \c NULL if \p text * is \c NULL). If \p text is not \c NULL, the return value is * guaranteed not to be \c NULL. * * \note There are libxml functions that purport to do this: * \c xmlEncodeEntitiesReentrant() and \c xmlEncodeSpecialChars(). * However, their escaping is incomplete. See: * https://discourse.gnome.org/t/intended-use-of-xmlencodeentitiesreentrant-vs-xmlencodespecialchars/19252 * \note The caller is responsible for freeing the return value using * \c g_free(). */ gchar * pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type) { GString *copy = NULL; if (text == NULL) { return NULL; } copy = g_string_sized_new(strlen(text)); while (*text != '\0') { // Don't escape any non-ASCII characters if ((*text & 0x80) != 0) { size_t bytes = g_utf8_next_char(text) - text; g_string_append_len(copy, text, bytes); text += bytes; continue; } switch (type) { case pcmk__xml_escape_text: switch (*text) { case '<': g_string_append(copy, PCMK__XML_ENTITY_LT); break; case '>': g_string_append(copy, PCMK__XML_ENTITY_GT); break; case '&': g_string_append(copy, PCMK__XML_ENTITY_AMP); break; case '\n': case '\t': g_string_append_c(copy, *text); break; default: if (g_ascii_iscntrl(*text)) { g_string_append_printf(copy, "&#x%.2X;", *text); } else { g_string_append_c(copy, *text); } break; } break; case pcmk__xml_escape_attr: switch (*text) { case '<': g_string_append(copy, PCMK__XML_ENTITY_LT); break; case '>': g_string_append(copy, PCMK__XML_ENTITY_GT); break; case '&': g_string_append(copy, PCMK__XML_ENTITY_AMP); break; case '"': g_string_append(copy, PCMK__XML_ENTITY_QUOT); break; default: if (g_ascii_iscntrl(*text)) { g_string_append_printf(copy, "&#x%.2X;", *text); } else { g_string_append_c(copy, *text); } break; } break; case pcmk__xml_escape_attr_pretty: switch (*text) { case '"': g_string_append(copy, "\\\""); break; case '\n': g_string_append(copy, "\\n"); break; case '\r': g_string_append(copy, "\\r"); break; case '\t': g_string_append(copy, "\\t"); break; default: g_string_append_c(copy, *text); break; } break; default: // Invalid enum value CRM_ASSERT(false); break; } text = g_utf8_next_char(text); } return g_string_free(copy, FALSE); } /*! * \internal * \brief Set a flag on all attributes of an XML element * * \param[in,out] xml XML node to set flags on * \param[in] flag XML private flag to set */ static void set_attrs_flag(xmlNode *xml, enum xml_private_flags flag) { for (xmlAttr *attr = pcmk__xe_first_attr(xml); attr; attr = attr->next) { pcmk__set_xml_flags((xml_node_private_t *) (attr->_private), flag); } } /*! * \internal * \brief Add an XML attribute to a node, marked as deleted * * When calculating XML changes, we need to know when an attribute has been * deleted. Add the attribute back to the new XML, so that we can check the * removal against ACLs, and mark it as deleted for later removal after * differences have been calculated. * * \param[in,out] new_xml XML to modify * \param[in] element Name of XML element that changed (for logging) * \param[in] attr_name Name of attribute that was deleted * \param[in] old_value Value of attribute that was deleted */ static void mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name, const char *old_value) { xml_doc_private_t *docpriv = new_xml->doc->_private; xmlAttr *attr = NULL; xml_node_private_t *nodepriv; - // Prevent the dirty flag being set recursively upwards + /* Restore the old value (without setting dirty flag recursively upwards or + * checking ACLs) + */ pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking); - - // Restore the old value (and the tracking flag) - attr = xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value); + crm_xml_add(new_xml, attr_name, old_value); pcmk__set_xml_flags(docpriv, pcmk__xf_tracking); // Reset flags (so the attribute doesn't appear as newly created) + attr = xmlHasProp(new_xml, (pcmkXmlStr) attr_name); nodepriv = attr->_private; nodepriv->flags = 0; // Check ACLs and mark restored value for later removal - remove_xe_attr(new_xml, attr); + pcmk__xa_remove(attr, false); crm_trace("XML attribute %s=%s was removed from %s", attr_name, old_value, element); } /* * \internal * \brief Check ACLs for a changed XML attribute */ static void mark_attr_changed(xmlNode *new_xml, const char *element, const char *attr_name, const char *old_value) { + xml_doc_private_t *docpriv = new_xml->doc->_private; char *vcopy = crm_element_value_copy(new_xml, attr_name); crm_trace("XML attribute %s was changed from '%s' to '%s' in %s", attr_name, old_value, vcopy, element); - // Restore the original value - xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value); + // Restore the original value (without checking ACLs) + pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking); + crm_xml_add(new_xml, attr_name, old_value); + pcmk__set_xml_flags(docpriv, pcmk__xf_tracking); // Change it back to the new value, to check ACLs crm_xml_add(new_xml, attr_name, vcopy); free(vcopy); } /*! * \internal * \brief Mark an XML attribute as having changed position * * \param[in,out] new_xml XML to modify * \param[in] element Name of XML element that changed (for logging) * \param[in,out] old_attr Attribute that moved, in original XML * \param[in,out] new_attr Attribute that moved, in \p new_xml * \param[in] p_old Ordinal position of \p old_attr in original XML * \param[in] p_new Ordinal position of \p new_attr in \p new_xml */ static void mark_attr_moved(xmlNode *new_xml, const char *element, xmlAttr *old_attr, xmlAttr *new_attr, int p_old, int p_new) { xml_node_private_t *nodepriv = new_attr->_private; crm_trace("XML attribute %s moved from position %d to %d in %s", old_attr->name, p_old, p_new, element); // Mark document, element, and all element's parents as changed pcmk__mark_xml_node_dirty(new_xml); // Mark attribute as changed pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_moved); nodepriv = (p_old > p_new)? old_attr->_private : new_attr->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); } /*! * \internal * \brief Calculate differences in all previously existing XML attributes * * \param[in,out] old_xml Original XML to compare * \param[in,out] new_xml New XML to compare */ static void xml_diff_old_attrs(xmlNode *old_xml, xmlNode *new_xml) { xmlAttr *attr_iter = pcmk__xe_first_attr(old_xml); while (attr_iter != NULL) { const char *name = (const char *) attr_iter->name; xmlAttr *old_attr = attr_iter; xmlAttr *new_attr = xmlHasProp(new_xml, attr_iter->name); const char *old_value = pcmk__xml_attr_value(attr_iter); attr_iter = attr_iter->next; if (new_attr == NULL) { mark_attr_deleted(new_xml, (const char *) old_xml->name, name, old_value); } else { xml_node_private_t *nodepriv = new_attr->_private; int new_pos = pcmk__xml_position((xmlNode*) new_attr, pcmk__xf_skip); int old_pos = pcmk__xml_position((xmlNode*) old_attr, pcmk__xf_skip); const char *new_value = crm_element_value(new_xml, name); // This attribute isn't new pcmk__clear_xml_flags(nodepriv, pcmk__xf_created); if (strcmp(new_value, old_value) != 0) { mark_attr_changed(new_xml, (const char *) old_xml->name, name, old_value); } else if ((old_pos != new_pos) && !pcmk__tracking_xml_changes(new_xml, TRUE)) { mark_attr_moved(new_xml, (const char *) old_xml->name, old_attr, new_attr, old_pos, new_pos); } } } } /*! * \internal * \brief Check all attributes in new XML for creation * * For each of a given XML element's attributes marked as newly created, accept * (and mark as dirty) or reject the creation according to ACLs. * * \param[in,out] new_xml XML to check */ static void mark_created_attrs(xmlNode *new_xml) { xmlAttr *attr_iter = pcmk__xe_first_attr(new_xml); while (attr_iter != NULL) { xmlAttr *new_attr = attr_iter; xml_node_private_t *nodepriv = attr_iter->_private; attr_iter = attr_iter->next; if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { const char *attr_name = (const char *) new_attr->name; crm_trace("Created new attribute %s=%s in %s", attr_name, pcmk__xml_attr_value(new_attr), new_xml->name); /* Check ACLs (we can't use the remove-then-create trick because it * would modify the attribute position). */ if (pcmk__check_acl(new_xml, attr_name, pcmk__xf_acl_write)) { pcmk__mark_xml_attr_dirty(new_attr); } else { // Creation was not allowed, so remove the attribute - xmlUnsetProp(new_xml, new_attr->name); + pcmk__xa_remove(new_attr, true); } } } } /*! * \internal * \brief Calculate differences in attributes between two XML nodes * * \param[in,out] old_xml Original XML to compare * \param[in,out] new_xml New XML to compare */ static void xml_diff_attrs(xmlNode *old_xml, xmlNode *new_xml) { set_attrs_flag(new_xml, pcmk__xf_created); // cleared later if not really new xml_diff_old_attrs(old_xml, new_xml); mark_created_attrs(new_xml); } /*! * \internal * \brief Add an XML child element to a node, marked as deleted * * When calculating XML changes, we need to know when a child element has been * deleted. Add the child back to the new XML, so that we can check the removal * against ACLs, and mark it as deleted for later removal after differences have * been calculated. * * \param[in,out] old_child Child element from original XML * \param[in,out] new_parent New XML to add marked copy to */ static void mark_child_deleted(xmlNode *old_child, xmlNode *new_parent) { // Re-create the child element so we can check ACLs xmlNode *candidate = pcmk__xml_copy(new_parent, old_child); // Clear flags on new child and its children pcmk__xml_tree_foreach(candidate, reset_xml_node_flags, NULL); // Check whether ACLs allow the deletion pcmk__apply_acl(xmlDocGetRootElement(candidate->doc)); // Remove the child again (which will track it in document's deleted_objs) free_xml_with_position(candidate, pcmk__xml_position(old_child, pcmk__xf_skip)); if (pcmk__xml_match(new_parent, old_child, true) == NULL) { pcmk__set_xml_flags((xml_node_private_t *) (old_child->_private), pcmk__xf_skip); } } static void mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child, int p_old, int p_new) { xml_node_private_t *nodepriv = new_child->_private; crm_trace("Child element %s with " PCMK_XA_ID "='%s' moved from position %d to %d under %s", new_child->name, pcmk__s(pcmk__xe_id(new_child), ""), p_old, p_new, new_parent->name); pcmk__mark_xml_node_dirty(new_parent); pcmk__set_xml_flags(nodepriv, pcmk__xf_moved); if (p_old > p_new) { nodepriv = old_child->_private; } else { nodepriv = new_child->_private; } pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); } // Given original and new XML, mark new XML portions that have changed static void mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top) { xmlNode *old_child = NULL; xmlNode *new_child = NULL; xml_node_private_t *nodepriv = NULL; CRM_CHECK(new_xml != NULL, return); if (old_xml == NULL) { - pcmk__xml_mark_created(new_xml); + mark_xml_tree_dirty_created(new_xml); pcmk__apply_creation_acl(new_xml, check_top); return; } nodepriv = new_xml->_private; CRM_CHECK(nodepriv != NULL, return); if(nodepriv->flags & pcmk__xf_processed) { /* Avoid re-comparing nodes */ return; } pcmk__set_xml_flags(nodepriv, pcmk__xf_processed); xml_diff_attrs(old_xml, new_xml); // Check for differences in the original children for (old_child = pcmk__xml_first_child(old_xml); old_child != NULL; old_child = pcmk__xml_next(old_child)) { new_child = pcmk__xml_match(new_xml, old_child, true); if (new_child != NULL) { mark_xml_changes(old_child, new_child, true); } else { mark_child_deleted(old_child, new_xml); } } // Check for moved or created children new_child = pcmk__xml_first_child(new_xml); while (new_child != NULL) { xmlNode *next = pcmk__xml_next(new_child); old_child = pcmk__xml_match(old_xml, new_child, true); if (old_child == NULL) { // This is a newly created child nodepriv = new_child->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); // May free new_child mark_xml_changes(old_child, new_child, true); } else { /* Check for movement, we already checked for differences */ int p_new = pcmk__xml_position(new_child, pcmk__xf_skip); int p_old = pcmk__xml_position(old_child, pcmk__xf_skip); if(p_old != p_new) { mark_child_moved(old_child, new_xml, new_child, p_old, p_new); } } new_child = next; } } void xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml) { pcmk__set_xml_doc_flag(new_xml, pcmk__xf_lazy); xml_calculate_changes(old_xml, new_xml); } // Called functions may set the \p pcmk__xf_skip flag on parts of \p old_xml void xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml) { CRM_CHECK((old_xml != NULL) && (new_xml != NULL) && pcmk__xe_is(old_xml, (const char *) new_xml->name) && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml), pcmk__str_none), return); if(xml_tracking_changes(new_xml) == FALSE) { xml_track_changes(new_xml, NULL, NULL, FALSE); } mark_xml_changes(old_xml, new_xml, FALSE); } -/*! - * \internal - * \brief Find a comment with matching content in specified XML - * - * \param[in] root XML to search - * \param[in] search_comment Comment whose content should be searched for - * \param[in] exact If true, comment must also be at same position - */ -xmlNode * -pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, bool exact) -{ - xmlNode *a_child = NULL; - int search_offset = pcmk__xml_position(search_comment, pcmk__xf_skip); - - CRM_CHECK(search_comment->type == XML_COMMENT_NODE, return NULL); - - for (a_child = pcmk__xml_first_child(root); a_child != NULL; - a_child = pcmk__xml_next(a_child)) { - if (exact) { - int offset = pcmk__xml_position(a_child, pcmk__xf_skip); - xml_node_private_t *nodepriv = a_child->_private; - - if (offset < search_offset) { - continue; - - } else if (offset > search_offset) { - return NULL; - } - - if (pcmk_is_set(nodepriv->flags, pcmk__xf_skip)) { - continue; - } - } - - if (a_child->type == XML_COMMENT_NODE - && pcmk__str_eq((const char *)a_child->content, (const char *)search_comment->content, pcmk__str_casei)) { - return a_child; - - } else if (exact) { - return NULL; - } - } - - return NULL; -} - -/*! - * \internal - * \brief Make one XML comment match another (in content) - * - * \param[in,out] parent If \p target is NULL and this is not, add or update - * comment child of this XML node that matches \p update - * \param[in,out] target If not NULL, update this XML comment node - * \param[in] update Make comment content match this (must not be NULL) - * - * \note At least one of \parent and \target must be non-NULL - */ -void -pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update) -{ - CRM_CHECK(update != NULL, return); - CRM_CHECK(update->type == XML_COMMENT_NODE, return); - - if (target == NULL) { - target = pcmk__xc_match(parent, update, false); - } - - if (target == NULL) { - pcmk__xml_copy(parent, update); - - } else if (!pcmk__str_eq((const char *)target->content, (const char *)update->content, pcmk__str_casei)) { - xmlFree(target->content); - target->content = xmlStrdup(update->content); - } -} - /*! * \internal * \brief Merge one XML tree into another * * Here, "merge" means: * 1. Copy attribute values from \p update to the target, overwriting in case of * conflict. * 2. Descend through \p update and the target in parallel. At each level, for * each child of \p update, look for a matching child of the target. * a. For each child, if a match is found, go to step 1, recursively merging * the child of \p update into the child of the target. * b. Otherwise, copy the child of \p update as a child of the target. * * A match is defined as the first child of the same type within the target, * with: * * the \c PCMK_XA_ID attribute matching, if set in \p update; otherwise, * * the \c PCMK_XA_ID_REF attribute matching, if set in \p update * * This function does not delete any elements or attributes from the target. It * may add elements or overwrite attributes, as described above. * * \param[in,out] parent If \p target is NULL and this is not, add or update * child of this XML node that matches \p update * \param[in,out] target If not NULL, update this XML * \param[in] update Make the desired XML match this (must not be \c NULL) * \param[in] flags Group of enum pcmk__xa_flags * * \note At least one of \p parent and \p target must be non-NULL. * \note This function is recursive. For the top-level call, \p parent is * \c NULL and \p target is not \c NULL. For recursive calls, \p target is * \c NULL and \p parent is not \c NULL. */ static void update_xe(xmlNode *parent, xmlNode *target, xmlNode *update, uint32_t flags) { // @TODO Try to refactor further, possibly using pcmk__xml_tree_foreach() const char *update_name = NULL; const char *update_id_attr = NULL; const char *update_id_val = NULL; char *trace_s = NULL; crm_log_xml_trace(update, "update"); crm_log_xml_trace(target, "target"); CRM_CHECK(update != NULL, goto done); if (update->type == XML_COMMENT_NODE) { pcmk__xc_update(parent, target, update); goto done; } update_name = (const char *) update->name; CRM_CHECK(update_name != NULL, goto done); CRM_CHECK((target != NULL) || (parent != NULL), goto done); update_id_val = pcmk__xe_id(update); if (update_id_val != NULL) { update_id_attr = PCMK_XA_ID; } else { update_id_val = crm_element_value(update, PCMK_XA_ID_REF); if (update_id_val != NULL) { update_id_attr = PCMK_XA_ID_REF; } } pcmk__if_tracing( { if (update_id_attr != NULL) { trace_s = crm_strdup_printf("<%s %s=%s/>", update_name, update_id_attr, update_id_val); } else { trace_s = crm_strdup_printf("<%s/>", update_name); } }, {} ); if (target == NULL) { // Recursive call target = pcmk__xe_first_child(parent, update_name, update_id_attr, update_id_val); } if (target == NULL) { // Recursive call with no existing matching child target = pcmk__xe_create(parent, update_name); crm_trace("Added %s", pcmk__s(trace_s, update_name)); } else { // Either recursive call with match, or top-level call crm_trace("Found node %s to update", pcmk__s(trace_s, update_name)); } CRM_CHECK(pcmk__xe_is(target, (const char *) update->name), return); pcmk__xe_copy_attrs(target, update, flags); for (xmlNode *child = pcmk__xml_first_child(update); child != NULL; child = pcmk__xml_next(child)) { crm_trace("Updating child of %s", pcmk__s(trace_s, update_name)); update_xe(target, NULL, child, flags); } crm_trace("Finished with %s", pcmk__s(trace_s, update_name)); done: free(trace_s); } /*! * \internal * \brief Delete an XML subtree if it matches a search element * * A match is defined as follows: * * \p xml and \p user_data are both element nodes of the same type. * * If \p user_data has attributes set, \p xml has those attributes set to the * same values. (\p xml may have additional attributes set to arbitrary * values.) * * \param[in,out] xml XML subtree to delete upon match * \param[in] user_data Search element * * \return \c true to continue traversing the tree, or \c false to stop (because * \p xml was deleted) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool delete_xe_if_matching(xmlNode *xml, void *user_data) { xmlNode *search = user_data; if (!pcmk__xe_is(search, (const char *) xml->name)) { // No match: either not both elements, or different element types return true; } for (const xmlAttr *attr = pcmk__xe_first_attr(search); attr != NULL; attr = attr->next) { const char *search_val = pcmk__xml_attr_value(attr); const char *xml_val = crm_element_value(xml, (const char *) attr->name); if (!pcmk__str_eq(search_val, xml_val, pcmk__str_casei)) { // No match: an attr in xml doesn't match the attr in search return true; } } crm_log_xml_trace(xml, "delete-match"); crm_log_xml_trace(search, "delete-search"); pcmk__xml_free(xml); // Found a match and deleted it; stop traversing tree return false; } /*! * \internal * \brief Search an XML tree depth-first and delete the first matching element * * This function does not attempt to match the tree root (\p xml). * * A match with a node \c node is defined as follows: * * \c node and \p search are both element nodes of the same type. * * If \p search has attributes set, \c node has those attributes set to the * same values. (\c node may have additional attributes set to arbitrary * values.) * * \param[in,out] xml XML subtree to search * \param[in] search Element to match against * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on * successful deletion and an error code otherwise) */ int pcmk__xe_delete_match(xmlNode *xml, xmlNode *search) { // See @COMPAT comment in pcmk__xe_replace_match() CRM_CHECK((xml != NULL) && (search != NULL), return EINVAL); for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL; xml = pcmk__xe_next(xml)) { if (!pcmk__xml_tree_foreach(xml, delete_xe_if_matching, search)) { // Found and deleted an element return pcmk_rc_ok; } } // No match found in this subtree return ENXIO; } /*! * \internal * \brief Replace one XML node with a copy of another XML node * * This function handles change tracking and applies ACLs. * * \param[in,out] old XML node to replace * \param[in] new XML node to copy as replacement for \p old * * \note This frees \p old. */ static void replace_node(xmlNode *old, xmlNode *new) { - new = xmlCopyNode(new, 1); - pcmk__mem_assert(new); + // Pass old for its doc; it won't remain the parent of new + new = pcmk__xml_copy(old, new); + old = xmlReplaceNode(old, new); + + // old == NULL means memory allocation error + CRM_ASSERT(old != NULL); // May be unnecessary but avoids slight changes to some test outputs pcmk__xml_tree_foreach(new, reset_xml_node_flags, NULL); - old = xmlReplaceNode(old, new); - if (xml_tracking_changes(new)) { // Replaced sections may have included relevant ACLs pcmk__apply_acl(new); } xml_calculate_changes(old, new); - xmlFreeNode(old); + free_xml_node(old); } /*! * \internal * \brief Replace one XML subtree with a copy of another if the two match * * A match is defined as follows: * * \p xml and \p user_data are both element nodes of the same type. * * If \p user_data has the \c PCMK_XA_ID attribute set, then \p xml has * \c PCMK_XA_ID set to the same value. * * \param[in,out] xml XML subtree to replace with \p user_data upon match * \param[in] user_data XML to replace \p xml with a copy of upon match * * \return \c true to continue traversing the tree, or \c false to stop (because * \p xml was replaced by \p user_data) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool replace_xe_if_matching(xmlNode *xml, void *user_data) { xmlNode *replace = user_data; const char *xml_id = NULL; const char *replace_id = NULL; xml_id = pcmk__xe_id(xml); replace_id = pcmk__xe_id(replace); if (!pcmk__xe_is(replace, (const char *) xml->name)) { // No match: either not both elements, or different element types return true; } if ((replace_id != NULL) && !pcmk__str_eq(replace_id, xml_id, pcmk__str_none)) { // No match: ID was provided in replace and doesn't match xml's ID return true; } crm_log_xml_trace(xml, "replace-match"); crm_log_xml_trace(replace, "replace-with"); replace_node(xml, replace); // Found a match and replaced it; stop traversing tree return false; } /*! * \internal * \brief Search an XML tree depth-first and replace the first matching element * * This function does not attempt to match the tree root (\p xml). * * A match with a node \c node is defined as follows: * * \c node and \p replace are both element nodes of the same type. * * If \p replace has the \c PCMK_XA_ID attribute set, then \c node has * \c PCMK_XA_ID set to the same value. * * \param[in,out] xml XML tree to search * \param[in] replace XML to replace a matching element with a copy of * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on * successful replacement and an error code otherwise) */ int pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace) { /* @COMPAT Some of this behavior (like not matching the tree root, which is * allowed by pcmk__xe_update_match()) is questionable for general use but * required for backward compatibility by cib_process_replace() and * cib_process_delete(). Behavior can change at a major version release if * desired. */ CRM_CHECK((xml != NULL) && (replace != NULL), return EINVAL); for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL; xml = pcmk__xe_next(xml)) { if (!pcmk__xml_tree_foreach(xml, replace_xe_if_matching, replace)) { // Found and replaced an element return pcmk_rc_ok; } } // No match found in this subtree return ENXIO; } //! User data for \c update_xe_if_matching() struct update_data { xmlNode *update; //!< Update source uint32_t flags; //!< Group of enum pcmk__xa_flags }; /*! * \internal * \brief Update one XML subtree with another if the two match * * "Update" means to merge a source subtree into a target subtree (see * \c update_xe()). * * A match is defined as follows: * * \p xml and \p user_data->update are both element nodes of the same type. * * \p xml and \p user_data->update have the same \c PCMK_XA_ID attribute * value, or \c PCMK_XA_ID is unset in both * * \param[in,out] xml XML subtree to update with \p user_data->update * upon match * \param[in] user_data struct update_data object * * \return \c true to continue traversing the tree, or \c false to stop (because * \p xml was updated by \p user_data->update) * * \note This is compatible with \c pcmk__xml_tree_foreach(). */ static bool update_xe_if_matching(xmlNode *xml, void *user_data) { struct update_data *data = user_data; xmlNode *update = data->update; if (!pcmk__xe_is(update, (const char *) xml->name)) { // No match: either not both elements, or different element types return true; } if (!pcmk__str_eq(pcmk__xe_id(xml), pcmk__xe_id(update), pcmk__str_none)) { // No match: ID mismatch return true; } crm_log_xml_trace(xml, "update-match"); crm_log_xml_trace(update, "update-with"); update_xe(NULL, xml, update, data->flags); // Found a match and replaced it; stop traversing tree return false; } /*! * \internal * \brief Search an XML tree depth-first and update the first matching element * * "Update" means to merge a source subtree into a target subtree (see * \c update_xe()). * * A match with a node \c node is defined as follows: * * \c node and \p update are both element nodes of the same type. * * \c node and \p update have the same \c PCMK_XA_ID attribute value, or * \c PCMK_XA_ID is unset in both * * \param[in,out] xml XML tree to search * \param[in] update XML to update a matching element with * \param[in] flags Group of enum pcmk__xa_flags * * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on * successful update and an error code otherwise) */ int pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags) { /* @COMPAT In pcmk__xe_delete_match() and pcmk__xe_replace_match(), we * compare IDs only if the equivalent of the update argument has an ID. * Here, we're stricter: we consider it a mismatch if only one element has * an ID attribute, or if both elements have IDs but they don't match. * * Perhaps we should align the behavior at a major version release. */ struct update_data data = { .update = update, .flags = flags, }; CRM_CHECK((xml != NULL) && (update != NULL), return EINVAL); if (!pcmk__xml_tree_foreach(xml, update_xe_if_matching, &data)) { // Found and updated an element return pcmk_rc_ok; } // No match found in this subtree return ENXIO; } /*! * \internal * \brief Get next sibling XML element with the same name as a given element * * \param[in] node XML element to start from * * \return Next sibling XML element with same name */ xmlNode * pcmk__xe_next_same(const xmlNode *node) { for (xmlNode *match = pcmk__xe_next(node); match != NULL; match = pcmk__xe_next(match)) { if (pcmk__xe_is(match, (const char *) node->name)) { return match; } } return NULL; } /*! * \internal * \brief Initialize the Pacemaker XML environment * * Set an XML buffer allocation scheme, set XML node create and destroy * callbacks, and load schemas into the cache. */ void pcmk__xml_init(void) { // @TODO Try to find a better caller than crm_log_preinit() static bool initialized = false; if (!initialized) { initialized = true; /* Double the buffer size when the buffer needs to grow. The default * allocator XML_BUFFER_ALLOC_EXACT was found to cause poor performance * due to the number of reallocs. */ xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT); - // Initialize private data at node creation - xmlRegisterNodeDefault(new_private_data); - - // Free private data at node destruction - xmlDeregisterNodeDefault(free_private_data); - // Load schemas into the cache pcmk__schema_init(); } } /*! * \internal * \brief Tear down the Pacemaker XML environment * * Destroy schema cache and clean up memory allocated by libxml2. */ void pcmk__xml_cleanup(void) { pcmk__schema_cleanup(); xmlCleanupParser(); } char * pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns) { static const char *base = NULL; char *ret = NULL; if (base == NULL) { base = pcmk__env_option(PCMK__ENV_SCHEMA_DIRECTORY); } if (pcmk__str_empty(base)) { base = PCMK_SCHEMA_DIR; } switch (ns) { case pcmk__xml_artefact_ns_legacy_rng: case pcmk__xml_artefact_ns_legacy_xslt: ret = strdup(base); break; case pcmk__xml_artefact_ns_base_rng: case pcmk__xml_artefact_ns_base_xslt: ret = crm_strdup_printf("%s/base", base); break; default: crm_err("XML artefact family specified as %u not recognized", ns); } return ret; } static char * find_artefact(enum pcmk__xml_artefact_ns ns, const char *path, const char *filespec) { char *ret = NULL; switch (ns) { case pcmk__xml_artefact_ns_legacy_rng: case pcmk__xml_artefact_ns_base_rng: if (pcmk__ends_with(filespec, ".rng")) { ret = crm_strdup_printf("%s/%s", path, filespec); } else { ret = crm_strdup_printf("%s/%s.rng", path, filespec); } break; case pcmk__xml_artefact_ns_legacy_xslt: case pcmk__xml_artefact_ns_base_xslt: if (pcmk__ends_with(filespec, ".xsl")) { ret = crm_strdup_printf("%s/%s", path, filespec); } else { ret = crm_strdup_printf("%s/%s.xsl", path, filespec); } break; default: crm_err("XML artefact family specified as %u not recognized", ns); } return ret; } char * pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec) { struct stat sb; char *base = pcmk__xml_artefact_root(ns); char *ret = NULL; ret = find_artefact(ns, base, filespec); free(base); if (stat(ret, &sb) != 0 || !S_ISREG(sb.st_mode)) { const char *remote_schema_dir = pcmk__remote_schema_dir(); free(ret); ret = find_artefact(ns, remote_schema_dir, filespec); } return ret; } void pcmk__xe_set_propv(xmlNodePtr node, va_list pairs) { while (true) { const char *name, *value; name = va_arg(pairs, const char *); if (name == NULL) { return; } value = va_arg(pairs, const char *); if (value != NULL) { crm_xml_add(node, name, value); } } } void pcmk__xe_set_props(xmlNodePtr node, ...) { va_list pairs; va_start(pairs, node); pcmk__xe_set_propv(node, pairs); va_end(pairs); } int pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name, int (*handler)(xmlNode *xml, void *userdata), void *userdata) { xmlNode *children = (xml? xml->children : NULL); CRM_ASSERT(handler != NULL); for (xmlNode *node = children; node != NULL; node = node->next) { if ((node->type == XML_ELEMENT_NODE) && ((child_element_name == NULL) || pcmk__xe_is(node, child_element_name))) { int rc = handler(node, userdata); if (rc != pcmk_rc_ok) { return rc; } } } return pcmk_rc_ok; } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START +#include #include xmlNode * copy_xml(xmlNode *src) { - xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION); + xmlDoc *doc = pcmk__xml_new_doc(); xmlNode *copy = NULL; - pcmk__mem_assert(doc); - copy = xmlDocCopyNode(src, doc, 1); pcmk__mem_assert(copy); xmlDocSetRootElement(doc, copy); + pcmk__xml_new_private_data(copy); return copy; } void crm_xml_init(void) { pcmk__xml_init(); } void crm_xml_cleanup(void) { pcmk__xml_cleanup(); } void pcmk_free_xml_subtree(xmlNode *xml) { - xmlUnlinkNode(xml); // Detaches from parent and siblings - xmlFreeNode(xml); // Frees + free_xml_node(xml); } void free_xml(xmlNode *child) { pcmk__xml_free(child); } xmlNode * expand_idref(xmlNode *input, xmlNode *top) { return pcmk__xe_resolve_idref(input, top); } void crm_xml_sanitize_id(char *id) { char *c; for (c = id; *c; ++c) { switch (*c) { case ':': case '#': *c = '.'; } } } 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, PCMK_XA_ID, id); free(id); } xmlNode * sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive) { xmlNode *child = NULL; GSList *nvpairs = NULL; xmlNode *result = NULL; CRM_CHECK(input != NULL, return NULL); result = pcmk__xe_create(parent, (const char *) input->name); nvpairs = pcmk_xml_attrs2nvpairs(input); nvpairs = pcmk_sort_nvpairs(nvpairs); pcmk_nvpairs2xml_attrs(nvpairs, result); pcmk_free_nvpairs(nvpairs); for (child = pcmk__xe_first_child(input, NULL, NULL, NULL); child != NULL; child = pcmk__xe_next(child)) { if (recursive) { sorted_xml(child, result, recursive); } else { pcmk__xml_copy(result, child); } } return result; } // LCOV_EXCL_STOP // End deprecated API diff --git a/lib/common/xml_attr.c b/lib/common/xml_attr.c index faed37f2b2..42d5a97ae0 100644 --- a/lib/common/xml_attr.c +++ b/lib/common/xml_attr.c @@ -1,95 +1,139 @@ /* * Copyright 2004-2024 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 /* xmlAllocOutputBuffer */ #include #include #include // PCMK__XML_LOG_BASE, etc. #include "crmcommon_private.h" +/*! + * \internal + * \brief Remove an XML attribute from its parent and free it + * + * \param[in,out] attr XML attribute to remove + * \param[in] force If \c true, remove the attribute immediately, ignoring + * ACLs and change tracking + * + * \return Standard Pacemaker return code (\c EPERM if ACLs prevent removal, or + * or \c pcmk_rc_ok otherwise) + * + * \note If the attribute has no parent element, this function does not free it. + * This mimics \c xmlRemoveProp(). + */ +int +pcmk__xa_remove(xmlAttr *attr, bool force) +{ + xmlNode *element = NULL; + + if ((attr == NULL) || (attr->parent == NULL)) { + return pcmk_rc_ok; + } + + element = attr->parent; + + if (!force && !pcmk__check_acl(element, NULL, pcmk__xf_acl_write)) { + // ACLs apply to element, not to particular attributes + crm_trace("ACLs prevent removal of attributes from %s element", + (const char *) element->name); + return EPERM; + } + + if (!force && pcmk__tracking_xml_changes(element, false)) { + // Leave in place (marked for removal) until after diff is calculated + pcmk__xml_set_parent_flags(element, pcmk__xf_dirty); + pcmk__set_xml_flags((xml_node_private_t *) attr->_private, + pcmk__xf_deleted); + } else { + pcmk__xml_free_private_data((xmlNode *) attr); + xmlRemoveProp(attr); + } + return pcmk_rc_ok; +} + void pcmk__mark_xml_attr_dirty(xmlAttr *a) { xmlNode *parent = a->parent; xml_node_private_t *nodepriv = a->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_modified); pcmk__clear_xml_flags(nodepriv, pcmk__xf_deleted); pcmk__mark_xml_node_dirty(parent); } // This also clears attribute's flags if not marked as deleted bool pcmk__marked_as_deleted(xmlAttrPtr a, void *user_data) { xml_node_private_t *nodepriv = a->_private; if (pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) { return true; } nodepriv->flags = pcmk__xf_none; return false; } /*! * \internal * \brief Append an XML attribute to a buffer * * \param[in] attr Attribute to append * \param[in,out] buffer Where to append the content (must not be \p NULL) */ void pcmk__dump_xml_attr(const xmlAttr *attr, GString *buffer) { const char *name = NULL; const char *value = NULL; gchar *value_esc = NULL; xml_node_private_t *nodepriv = NULL; if (attr == NULL || attr->children == NULL) { return; } nodepriv = attr->_private; if (nodepriv && pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) { return; } name = (const char *) attr->name; value = (const char *) attr->children->content; if (value == NULL) { /* Don't print anything for unset attribute. Any null-indicator value, * including the empty string, could also be a real value that needs to * be treated differently from "unset". */ return; } if (pcmk__xml_needs_escape(value, pcmk__xml_escape_attr)) { value_esc = pcmk__xml_escape(value, pcmk__xml_escape_attr); value = value_esc; } pcmk__g_strcat(buffer, " ", name, "=\"", value, "\"", NULL); g_free(value_esc); } diff --git a/lib/common/xml_comment.c b/lib/common/xml_comment.c new file mode 100644 index 0000000000..e5211a27b9 --- /dev/null +++ b/lib/common/xml_comment.c @@ -0,0 +1,115 @@ +/* + * Copyright 2024 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 // NULL + +#include // xmlDoc, xmlNode, etc. + +#include "crmcommon_private.h" + +/*! + * \internal + * \brief Create a new XML comment belonging to a given document + * + * \param[in] doc Document that new comment will belong to + * \param[in] content Comment content + * + * \return Newly created XML comment (guaranteed not to be \c NULL) + */ +xmlNode * +pcmk__xc_create(xmlDoc *doc, const char *content) +{ + xmlNode *node = NULL; + + // Pacemaker typically assumes every xmlNode has a doc + CRM_ASSERT(doc != NULL); + + node = xmlNewDocComment(doc, (pcmkXmlStr) content); + pcmk__mem_assert(node); + pcmk__xml_new_private_data(node); + return node; +} + +/*! + * \internal + * \brief Find a comment with matching content in specified XML + * + * \param[in] root XML to search + * \param[in] search_comment Comment whose content should be searched for + * \param[in] exact If true, comment must also be at same position + */ +xmlNode * +pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, bool exact) +{ + xmlNode *a_child = NULL; + int search_offset = pcmk__xml_position(search_comment, pcmk__xf_skip); + + CRM_CHECK(search_comment->type == XML_COMMENT_NODE, return NULL); + + for (a_child = pcmk__xml_first_child(root); a_child != NULL; + a_child = pcmk__xml_next(a_child)) { + if (exact) { + int offset = pcmk__xml_position(a_child, pcmk__xf_skip); + xml_node_private_t *nodepriv = a_child->_private; + + if (offset < search_offset) { + continue; + + } else if (offset > search_offset) { + return NULL; + } + + if (pcmk_is_set(nodepriv->flags, pcmk__xf_skip)) { + continue; + } + } + + if (a_child->type == XML_COMMENT_NODE + && pcmk__str_eq((const char *)a_child->content, (const char *)search_comment->content, pcmk__str_casei)) { + return a_child; + + } else if (exact) { + return NULL; + } + } + + return NULL; +} + +/*! + * \internal + * \brief Make one XML comment match another (in content) + * + * \param[in,out] parent If \p target is NULL and this is not, add or update + * comment child of this XML node that matches \p update + * \param[in,out] target If not NULL, update this XML comment node + * \param[in] update Make comment content match this (must not be NULL) + * + * \note At least one of \parent and \target must be non-NULL + */ +void +pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update) +{ + CRM_CHECK(update != NULL, return); + CRM_CHECK(update->type == XML_COMMENT_NODE, return); + + if (target == NULL) { + target = pcmk__xc_match(parent, update, false); + } + + if (target == NULL) { + pcmk__xml_copy(parent, update); + + } else if (!pcmk__str_eq((const char *)target->content, (const char *)update->content, pcmk__str_casei)) { + xmlFree(target->content); + target->content = xmlStrdup(update->content); + } +} diff --git a/lib/common/xml_io.c b/lib/common/xml_io.c index 1a596f0f15..01dba55247 100644 --- a/lib/common/xml_io.c +++ b/lib/common/xml_io.c @@ -1,737 +1,739 @@ /* * Copyright 2004-2024 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 // xmlOutputBuffer* #include #include #include #include "crmcommon_private.h" /* @COMPAT XML_PARSE_RECOVER allows some XML errors to be silently worked around * by libxml2, which is potentially ambiguous and dangerous. We should drop it * when we can break backward compatibility with configurations that might be * relying on it (i.e. pacemaker 3.0.0). */ #define PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER (XML_PARSE_NOBLANKS) #define PCMK__XML_PARSE_OPTS_WITH_RECOVER (XML_PARSE_NOBLANKS \ |XML_PARSE_RECOVER) /*! * \internal * \brief Read from \c stdin until EOF or error * * \return Newly allocated string containing the bytes read from \c stdin, or * \c NULL on error * * \note The caller is responsible for freeing the return value using \c free(). */ static char * read_stdin(void) { char *buf = NULL; size_t length = 0; do { buf = pcmk__realloc(buf, length + PCMK__BUFFER_SIZE + 1); length += fread(buf + length, 1, PCMK__BUFFER_SIZE, stdin); } while ((feof(stdin) == 0) && (ferror(stdin) == 0)); if (ferror(stdin) != 0) { crm_err("Error reading input from stdin"); free(buf); buf = NULL; } else { buf[length] = '\0'; } clearerr(stdin); return buf; } /*! * \internal * \brief Decompress a bzip2-compressed file into a string buffer * * \param[in] filename Name of file to decompress * * \return Newly allocated string with the decompressed contents of \p filename, * or \c NULL on error. * * \note The caller is responsible for freeing the return value using \c free(). */ static char * decompress_file(const char *filename) { char *buffer = NULL; int rc = pcmk_rc_ok; size_t length = 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); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_err("Could not prepare to read compressed %s: %s " QB_XS " rc=%d", filename, pcmk_rc_str(rc), rc); goto done; } // cppcheck seems not to understand the abort-logic in pcmk__realloc // cppcheck-suppress memleak do { int read_len = 0; buffer = pcmk__realloc(buffer, length + PCMK__BUFFER_SIZE + 1); read_len = BZ2_bzRead(&rc, bz_file, buffer + length, PCMK__BUFFER_SIZE); if ((rc == BZ_OK) || (rc == BZ_STREAM_END)) { crm_trace("Read %ld bytes from file: %d", (long) read_len, rc); length += read_len; } } while (rc == BZ_OK); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { rc = pcmk__bzlib2rc(rc); crm_err("Could not read compressed %s: %s " QB_XS " rc=%d", filename, pcmk_rc_str(rc), rc); free(buffer); buffer = NULL; } else { buffer[length] = '\0'; } done: BZ2_bzReadClose(&rc, bz_file); fclose(input); return buffer; } // @COMPAT Remove macro at 3.0.0 when we drop XML_PARSE_RECOVER /*! * \internal * \brief Try to parse XML first without and then with recovery enabled * * \param[out] result Where to store the resulting XML doc (xmlDoc **) * \param[in] fn XML parser function * \param[in] ... All arguments for \p fn except the final one (an * \c xmlParserOption group) */ #define parse_xml_recover(result, fn, ...) do { \ *result = fn(__VA_ARGS__, PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); \ if (*result == NULL) { \ *result = fn(__VA_ARGS__, PCMK__XML_PARSE_OPTS_WITH_RECOVER); \ \ if (*result != NULL) { \ crm_warn("Successfully recovered from XML errors " \ "(note: a future release will treat this as a " \ "fatal failure)"); \ } \ } \ } while (0); /*! * \internal * \brief Parse XML from a file * * \param[in] filename Name of file containing XML (\c NULL or \c "-" for * \c stdin); if \p filename ends in \c ".bz2", the file * will be decompressed using \c bzip2 * * \return XML tree parsed from the given file; may be \c NULL or only partial * on error */ xmlNode * pcmk__xml_read(const char *filename) { bool use_stdin = pcmk__str_eq(filename, "-", pcmk__str_null_matches); xmlNode *xml = NULL; xmlDoc *output = NULL; xmlParserCtxt *ctxt = NULL; const xmlError *last_error = NULL; // Create a parser context ctxt = xmlNewParserCtxt(); CRM_CHECK(ctxt != NULL, return NULL); xmlCtxtResetLastError(ctxt); xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); if (use_stdin) { /* @COMPAT After dropping XML_PARSE_RECOVER, we can avoid capturing * stdin into a buffer and instead call * xmlCtxtReadFd(ctxt, STDIN_FILENO, NULL, NULL, XML_PARSE_NOBLANKS); * * For now we have to save the input so that we can use it twice. */ char *input = read_stdin(); if (input != NULL) { parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input, NULL, NULL); free(input); } } else if (pcmk__ends_with_ext(filename, ".bz2")) { char *input = decompress_file(filename); if (input != NULL) { parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input, NULL, NULL); free(input); } } else { parse_xml_recover(&output, xmlCtxtReadFile, ctxt, filename, NULL); } if (output != NULL) { + pcmk__xml_new_private_data((xmlNode *) output); xml = xmlDocGetRootElement(output); if (xml != NULL) { /* @TODO Should we really be stripping out text? This seems like an * overly broad way to get rid of whitespace, if that's the goal. * Text nodes may be invalid in most or all Pacemaker inputs, but * stripping them in a generic "parse XML from file" function may * not be the best way to ignore them. */ pcmk__strip_xml_text(xml); } } // @COMPAT At 3.0.0, free xml and return NULL if xml != NULL on error last_error = xmlCtxtGetLastError(ctxt); if (last_error != NULL) { if (xml != NULL) { crm_log_xml_info(xml, "Partial"); } } xmlFreeParserCtxt(ctxt); return xml; } /*! * \internal * \brief Parse XML from a string * * \param[in] input String to parse * * \return XML tree parsed from the given string; may be \c NULL or only partial * on error */ xmlNode * pcmk__xml_parse(const char *input) { xmlNode *xml = NULL; xmlDoc *output = NULL; xmlParserCtxt *ctxt = NULL; const xmlError *last_error = NULL; if (input == NULL) { return NULL; } ctxt = xmlNewParserCtxt(); if (ctxt == NULL) { return NULL; } xmlCtxtResetLastError(ctxt); xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); parse_xml_recover(&output, xmlCtxtReadDoc, ctxt, (pcmkXmlStr) input, NULL, NULL); if (output != NULL) { + pcmk__xml_new_private_data((xmlNode *) output); xml = xmlDocGetRootElement(output); } // @COMPAT At 3.0.0, free xml and return NULL if xml != NULL; update doxygen last_error = xmlCtxtGetLastError(ctxt); if (last_error != NULL) { if (xml != NULL) { crm_log_xml_info(xml, "Partial"); } } xmlFreeParserCtxt(ctxt); return xml; } /*! * \internal * \brief Append a string representation of an XML element to a buffer * * \param[in] data XML whose representation to append * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_element(const xmlNode *data, uint32_t options, GString *buffer, int depth) { bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); bool filtered = pcmk_is_set(options, pcmk__xml_fmt_filtered); int spaces = pretty? (2 * depth) : 0; for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "<", data->name, NULL); for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; attr = attr->next) { if (!filtered || !pcmk__xa_filterable((const char *) (attr->name))) { pcmk__dump_xml_attr(attr, buffer); } } if (data->children == NULL) { g_string_append(buffer, "/>"); } else { g_string_append_c(buffer, '>'); } if (pretty) { g_string_append_c(buffer, '\n'); } if (data->children) { for (const xmlNode *child = data->children; child != NULL; child = child->next) { pcmk__xml_string(child, options, buffer, depth + 1); } for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "name, ">", NULL); if (pretty) { g_string_append_c(buffer, '\n'); } } } /*! * \internal * \brief Append XML text content to a buffer * * \param[in] data XML whose content to append * \param[in] options Group of enum pcmk__xml_fmt_options * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_text(const xmlNode *data, uint32_t options, GString *buffer, int depth) { bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); int spaces = pretty? (2 * depth) : 0; const char *content = (const char *) data->content; gchar *content_esc = NULL; if (pcmk__xml_needs_escape(content, pcmk__xml_escape_text)) { content_esc = pcmk__xml_escape(content, pcmk__xml_escape_text); content = content_esc; } for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } g_string_append(buffer, content); if (pretty) { g_string_append_c(buffer, '\n'); } g_free(content_esc); } /*! * \internal * \brief Append XML CDATA content to a buffer * * \param[in] data XML whose content to append * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_cdata(const xmlNode *data, uint32_t options, GString *buffer, int depth) { bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); int spaces = pretty? (2 * depth) : 0; for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "content, "]]>", NULL); if (pretty) { g_string_append_c(buffer, '\n'); } } /*! * \internal * \brief Append an XML comment to a buffer * * \param[in] data XML whose content to append * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to append the content (must not be \p NULL) * \param[in] depth Current indentation level */ static void dump_xml_comment(const xmlNode *data, uint32_t options, GString *buffer, int depth) { bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); int spaces = pretty? (2 * depth) : 0; for (int lpc = 0; lpc < spaces; lpc++) { g_string_append_c(buffer, ' '); } pcmk__g_strcat(buffer, "", NULL); if (pretty) { g_string_append_c(buffer, '\n'); } } /*! * \internal * \brief Get a string representation of an XML element type * * \param[in] type XML element type * * \return String representation of \p type */ static const char * xml_element_type_text(xmlElementType type) { static const char *const element_type_names[] = { [XML_ELEMENT_NODE] = "element", [XML_ATTRIBUTE_NODE] = "attribute", [XML_TEXT_NODE] = "text", [XML_CDATA_SECTION_NODE] = "CDATA section", [XML_ENTITY_REF_NODE] = "entity reference", [XML_ENTITY_NODE] = "entity", [XML_PI_NODE] = "PI", [XML_COMMENT_NODE] = "comment", [XML_DOCUMENT_NODE] = "document", [XML_DOCUMENT_TYPE_NODE] = "document type", [XML_DOCUMENT_FRAG_NODE] = "document fragment", [XML_NOTATION_NODE] = "notation", [XML_HTML_DOCUMENT_NODE] = "HTML document", [XML_DTD_NODE] = "DTD", [XML_ELEMENT_DECL] = "element declaration", [XML_ATTRIBUTE_DECL] = "attribute declaration", [XML_ENTITY_DECL] = "entity declaration", [XML_NAMESPACE_DECL] = "namespace declaration", [XML_XINCLUDE_START] = "XInclude start", [XML_XINCLUDE_END] = "XInclude end", }; if ((type < 0) || (type >= PCMK__NELEM(element_type_names))) { return "unrecognized type"; } return element_type_names[type]; } /*! * \internal * \brief Create a string representation of an XML object * * libxml2's \c xmlNodeDumpOutput() doesn't allow filtering, doesn't escape * special characters thoroughly, and doesn't allow a const argument. * * \param[in] data XML to convert * \param[in] options Group of \p pcmk__xml_fmt_options flags * \param[in,out] buffer Where to store the text (must not be \p NULL) * \param[in] depth Current indentation level * * \todo Create a wrapper that doesn't require \p depth. Only used with * recursive calls currently. */ void pcmk__xml_string(const xmlNode *data, uint32_t options, GString *buffer, int depth) { if (data == NULL) { crm_trace("Nothing to dump"); return; } CRM_ASSERT(buffer != NULL); CRM_CHECK(depth >= 0, depth = 0); switch(data->type) { case XML_ELEMENT_NODE: /* Handle below */ dump_xml_element(data, options, buffer, depth); break; case XML_TEXT_NODE: if (pcmk_is_set(options, pcmk__xml_fmt_text)) { dump_xml_text(data, options, buffer, depth); } break; case XML_COMMENT_NODE: dump_xml_comment(data, options, buffer, depth); break; case XML_CDATA_SECTION_NODE: dump_xml_cdata(data, options, buffer, depth); break; default: crm_warn("Cannot convert XML %s node to text " QB_XS " type=%d", xml_element_type_text(data->type), data->type); break; } } /*! * \internal * \brief Write a string to a file stream, compressed using \c bzip2 * * \param[in] text String to write * \param[in] filename Name of file being written (for logging only) * \param[in,out] stream Open file stream to write to * \param[out] bytes_out Number of bytes written (valid only on success) * * \return Standard Pacemaker return code */ static int write_compressed_stream(char *text, const char *filename, FILE *stream, unsigned int *bytes_out) { unsigned int bytes_in = 0; int rc = pcmk_rc_ok; // (5, 0, 0): (intermediate block size, silent, default workFactor) BZFILE *bz_file = BZ2_bzWriteOpen(&rc, stream, 5, 0, 0); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_warn("Not compressing %s: could not prepare file stream: %s " QB_XS " rc=%d", filename, pcmk_rc_str(rc), rc); goto done; } BZ2_bzWrite(&rc, bz_file, text, strlen(text)); rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_warn("Not compressing %s: could not compress data: %s " QB_XS " rc=%d errno=%d", filename, pcmk_rc_str(rc), rc, errno); goto done; } BZ2_bzWriteClose(&rc, bz_file, 0, &bytes_in, bytes_out); bz_file = NULL; rc = pcmk__bzlib2rc(rc); if (rc != pcmk_rc_ok) { crm_warn("Not compressing %s: could not write compressed data: %s " QB_XS " rc=%d errno=%d", filename, pcmk_rc_str(rc), rc, errno); goto done; } crm_trace("Compressed XML for %s from %u bytes to %u", filename, bytes_in, *bytes_out); done: if (bz_file != NULL) { BZ2_bzWriteClose(&rc, bz_file, 0, NULL, NULL); } return rc; } /*! * \internal * \brief Write XML to a file stream * * \param[in] xml XML to write * \param[in] filename Name of file being written (for logging only) * \param[in,out] stream Open file stream corresponding to filename (closed * when this function returns) * \param[in] compress Whether to compress XML before writing * * \return Standard Pacemaker return code */ static int write_xml_stream(const xmlNode *xml, const char *filename, FILE *stream, bool compress) { GString *buffer = g_string_sized_new(1024); unsigned int bytes_out = 0; int rc = pcmk_rc_ok; pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buffer, 0); CRM_CHECK(!pcmk__str_empty(buffer->str), crm_log_xml_info(xml, "dump-failed"); rc = pcmk_rc_error; goto done); crm_log_xml_trace(xml, "writing"); if (compress && (write_compressed_stream(buffer->str, filename, stream, &bytes_out) == pcmk_rc_ok)) { goto done; } rc = fprintf(stream, "%s", buffer->str); if (rc < 0) { rc = EIO; crm_perror(LOG_ERR, "writing %s", filename); goto done; } bytes_out = (unsigned int) rc; rc = pcmk_rc_ok; done: if (fflush(stream) != 0) { rc = 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)) { rc = errno; crm_perror(LOG_ERR, "synchronizing %s", filename); } fclose(stream); crm_trace("Saved %u bytes to %s as XML", bytes_out, filename); g_string_free(buffer, TRUE); return rc; } /*! * \internal * \brief Write XML to a file descriptor * * \param[in] xml XML to write * \param[in] filename Name of file being written (for logging only) * \param[in] fd Open file descriptor corresponding to \p filename * * \return Standard Pacemaker return code */ int pcmk__xml_write_fd(const xmlNode *xml, const char *filename, int fd) { FILE *stream = NULL; CRM_CHECK((xml != NULL) && (fd > 0), return EINVAL); stream = fdopen(fd, "w"); if (stream == NULL) { return errno; } return write_xml_stream(xml, pcmk__s(filename, "unnamed file"), stream, false); } /*! * \internal * \brief Write XML to a file * * \param[in] xml XML to write * \param[in] filename Name of file to write * \param[in] compress If \c true, compress XML before writing * * \return Standard Pacemaker return code */ int pcmk__xml_write_file(const xmlNode *xml, const char *filename, bool compress) { FILE *stream = NULL; CRM_CHECK((xml != NULL) && (filename != NULL), return EINVAL); stream = fopen(filename, "w"); if (stream == NULL) { return errno; } return write_xml_stream(xml, filename, stream, compress); } /*! * \internal * \brief Serialize XML (using libxml) into provided descriptor * * \param[in] fd File descriptor to (piece-wise) write to * \param[in] cur XML subtree to proceed * * \return a standard Pacemaker return code */ int pcmk__xml2fd(int fd, xmlNode *cur) { bool success; xmlOutputBuffer *fd_out = xmlOutputBufferCreateFd(fd, NULL); pcmk__mem_assert(fd_out); xmlNodeDumpOutput(fd_out, cur->doc, cur, 0, pcmk__xml_fmt_pretty, NULL); success = xmlOutputBufferWrite(fd_out, sizeof("\n") - 1, "\n") != -1; success = xmlOutputBufferClose(fd_out) != -1 && success; if (!success) { return EIO; } fsync(fd); return pcmk_rc_ok; } void save_xml_to_file(const 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", pcmk__get_tmpdir(), uuid); filename = f; free(uuid); } crm_info("Saving %s to %s", desc, filename); pcmk__xml_write_file(xml, filename, false); free(f); } diff --git a/lib/pacemaker/pcmk_acl.c b/lib/pacemaker/pcmk_acl.c index bd2c39288c..2ff57d2155 100644 --- a/lib/pacemaker/pcmk_acl.c +++ b/lib/pacemaker/pcmk_acl.c @@ -1,397 +1,398 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define ACL_NS_PREFIX "http://clusterlabs.org/ns/pacemaker/access/" #define ACL_NS_Q_PREFIX "pcmk-access-" #define ACL_NS_Q_WRITABLE (const xmlChar *) ACL_NS_Q_PREFIX "writable" #define ACL_NS_Q_READABLE (const xmlChar *) ACL_NS_Q_PREFIX "readable" #define ACL_NS_Q_DENIED (const xmlChar *) ACL_NS_Q_PREFIX "denied" static const xmlChar *NS_WRITABLE = (const xmlChar *) ACL_NS_PREFIX "writable"; static const xmlChar *NS_READABLE = (const xmlChar *) ACL_NS_PREFIX "readable"; static const xmlChar *NS_DENIED = (const xmlChar *) ACL_NS_PREFIX "denied"; /*! * \brief This function takes a node and marks it with the namespace * given in the ns parameter. * * \param[in,out] i_node * \param[in] ns * \param[in,out] ret * \param[in,out] ns_recycle_writable * \param[in,out] ns_recycle_readable * \param[in,out] ns_recycle_denied */ static void pcmk__acl_mark_node_with_namespace(xmlNode *i_node, const xmlChar *ns, int *ret, xmlNs **ns_recycle_writable, xmlNs **ns_recycle_readable, xmlNs **ns_recycle_denied) { if (ns == NS_WRITABLE) { if (*ns_recycle_writable == NULL) { *ns_recycle_writable = xmlNewNs(xmlDocGetRootElement(i_node->doc), NS_WRITABLE, ACL_NS_Q_WRITABLE); } xmlSetNs(i_node, *ns_recycle_writable); *ret = pcmk_rc_ok; } else if (ns == NS_READABLE) { if (*ns_recycle_readable == NULL) { *ns_recycle_readable = xmlNewNs(xmlDocGetRootElement(i_node->doc), NS_READABLE, ACL_NS_Q_READABLE); } xmlSetNs(i_node, *ns_recycle_readable); *ret = pcmk_rc_ok; } else if (ns == NS_DENIED) { if (*ns_recycle_denied == NULL) { *ns_recycle_denied = xmlNewNs(xmlDocGetRootElement(i_node->doc), NS_DENIED, ACL_NS_Q_DENIED); }; xmlSetNs(i_node, *ns_recycle_denied); *ret = pcmk_rc_ok; } } /*! * \brief Annotate a given XML element or property and its siblings with * XML namespaces to indicate ACL permissions * * \param[in,out] xml_modify XML to annotate * * \return A standard Pacemaker return code * Namely: * - pcmk_rc_ok upon success, * - pcmk_rc_already if ACLs were not applicable, * - pcmk_rc_schema_validation if the validation schema version * is unsupported (see note), or * - EINVAL or ENOMEM as appropriate; * * \note This function is recursive */ static int annotate_with_siblings(xmlNode *xml_modify) { static xmlNs *ns_recycle_writable = NULL, *ns_recycle_readable = NULL, *ns_recycle_denied = NULL; static const xmlDoc *prev_doc = NULL; xmlNode *i_node = NULL; const xmlChar *ns; int ret = EINVAL; // nodes have not been processed yet if (prev_doc == NULL || prev_doc != xml_modify->doc) { prev_doc = xml_modify->doc; ns_recycle_writable = ns_recycle_readable = ns_recycle_denied = NULL; } for (i_node = xml_modify; i_node != NULL; i_node = i_node->next) { switch (i_node->type) { case XML_ELEMENT_NODE: pcmk__set_xml_doc_flag(i_node, pcmk__xf_tracking); if (!pcmk__check_acl(i_node, NULL, pcmk__xf_acl_read)) { ns = NS_DENIED; } else if (!pcmk__check_acl(i_node, NULL, pcmk__xf_acl_write)) { ns = NS_READABLE; } else { ns = NS_WRITABLE; } pcmk__acl_mark_node_with_namespace(i_node, ns, &ret, &ns_recycle_writable, &ns_recycle_readable, &ns_recycle_denied); // @TODO Could replace recursion with iteration to save stack if (i_node->properties != NULL) { /* This is not entirely clear, but relies on the very same * class-hierarchy emulation that libxml2 has firmly baked * in its API/ABI */ ret |= annotate_with_siblings((xmlNodePtr) i_node->properties); } if (i_node->children != NULL) { ret |= annotate_with_siblings(i_node->children); } break; case XML_ATTRIBUTE_NODE: // We can utilize that parent has already been assigned the ns if (!pcmk__check_acl(i_node->parent, (const char *) i_node->name, pcmk__xf_acl_read)) { ns = NS_DENIED; } else if (!pcmk__check_acl(i_node, (const char *) i_node->name, pcmk__xf_acl_write)) { ns = NS_READABLE; } else { ns = NS_WRITABLE; } pcmk__acl_mark_node_with_namespace(i_node, ns, &ret, &ns_recycle_writable, &ns_recycle_readable, &ns_recycle_denied); break; case XML_COMMENT_NODE: // We can utilize that parent has already been assigned the ns if (!pcmk__check_acl(i_node->parent, (const char *) i_node->name, pcmk__xf_acl_read)) { ns = NS_DENIED; } else if (!pcmk__check_acl(i_node->parent, (const char *) i_node->name, pcmk__xf_acl_write)) { ns = NS_READABLE; } else { ns = NS_WRITABLE; } pcmk__acl_mark_node_with_namespace(i_node, ns, &ret, &ns_recycle_writable, &ns_recycle_readable, &ns_recycle_denied); break; default: break; } } return ret; } int pcmk__acl_annotate_permissions(const char *cred, const xmlDoc *cib_doc, xmlDoc **acl_evaled_doc) { int ret; xmlNode *target, *comment; const char *validation; CRM_CHECK(cred != NULL, return EINVAL); CRM_CHECK(cib_doc != NULL, return EINVAL); CRM_CHECK(acl_evaled_doc != NULL, return EINVAL); /* avoid trivial accidental XML injection */ if (strpbrk(cred, "<>&") != NULL) { return EINVAL; } if (!pcmk_acl_required(cred)) { /* nothing to evaluate */ return pcmk_rc_already; } validation = crm_element_value(xmlDocGetRootElement(cib_doc), PCMK_XA_VALIDATE_WITH); if (pcmk__cmp_schemas_by_name(PCMK__COMPAT_ACL_2_MIN_INCL, validation) > 0) { return pcmk_rc_schema_validation; } target = pcmk__xml_copy(NULL, xmlDocGetRootElement((xmlDoc *) cib_doc)); if (target == NULL) { return EINVAL; } pcmk__enable_acl(target, target, cred); ret = annotate_with_siblings(target); if (ret == pcmk_rc_ok) { - char *credentials = crm_strdup_printf("ACLs as evaluated for user %s", - cred); - - comment = xmlNewDocComment(target->doc, (pcmkXmlStr) credentials); - free(credentials); - if (comment == NULL) { - xmlFreeNode(target); - return EINVAL; - } + char *content = crm_strdup_printf("ACLs as evaluated for user %s", + cred); + + comment = pcmk__xc_create(target->doc, content); xmlAddPrevSibling(xmlDocGetRootElement(target->doc), comment); *acl_evaled_doc = target->doc; - return pcmk_rc_ok; + free(content); + } else { - xmlFreeNode(target); - return ret; //for now, it should be some kind of error + pcmk__xml_free(target); } + return ret; } int pcmk__acl_evaled_render(xmlDoc *annotated_doc, enum pcmk__acl_render_how how, xmlChar **doc_txt_ptr) { xmlDoc *xslt_doc; xsltStylesheet *xslt; xsltTransformContext *xslt_ctxt; xmlDoc *res; char *sfile; static const char *params_namespace[] = { "accessrendercfg:c-writable", ACL_NS_Q_PREFIX "writable:", "accessrendercfg:c-readable", ACL_NS_Q_PREFIX "readable:", "accessrendercfg:c-denied", ACL_NS_Q_PREFIX "denied:", "accessrendercfg:c-reset", "", "accessrender:extra-spacing", "no", "accessrender:self-reproducing-prefix", ACL_NS_Q_PREFIX, NULL }, *params_useansi[] = { /* start with hard-coded defaults, then adapt per the template ones */ "accessrendercfg:c-writable", "\x1b[32m", "accessrendercfg:c-readable", "\x1b[34m", "accessrendercfg:c-denied", "\x1b[31m", "accessrendercfg:c-reset", "\x1b[0m", "accessrender:extra-spacing", "no", "accessrender:self-reproducing-prefix", ACL_NS_Q_PREFIX, NULL }, *params_noansi[] = { "accessrendercfg:c-writable", "vvv---[ WRITABLE ]---vvv", "accessrendercfg:c-readable", "vvv---[ READABLE ]---vvv", "accessrendercfg:c-denied", "vvv---[ ~DENIED~ ]---vvv", "accessrendercfg:c-reset", "", "accessrender:extra-spacing", "yes", "accessrender:self-reproducing-prefix", "", NULL }; const char **params; int rc = pcmk_rc_ok; xmlParserCtxtPtr parser_ctxt; /* unfortunately, the input (coming from CIB originally) was parsed with blanks ignored, and since the output is a conversion of XML to text format (we would be covered otherwise thanks to implicit pretty-printing), we need to dump the tree to string output first, only to subsequently reparse it -- this time with blanks honoured */ xmlChar *annotated_dump; int dump_size; CRM_ASSERT(how != pcmk__acl_render_none); // Color is the default render mode for terminals; text is default otherwise if (how == pcmk__acl_render_default) { if (isatty(STDOUT_FILENO)) { how = pcmk__acl_render_color; } else { how = pcmk__acl_render_text; } } xmlDocDumpFormatMemory(annotated_doc, &annotated_dump, &dump_size, 1); + + /* res does not need private data: it's temporary and used only with libxslt + * functions + */ res = xmlReadDoc(annotated_dump, "on-the-fly-access-render", NULL, XML_PARSE_NONET); CRM_ASSERT(res != NULL); xmlFree(annotated_dump); - xmlFreeDoc(annotated_doc); + pcmk__xml_free_doc(annotated_doc); annotated_doc = res; sfile = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_base_xslt, "access-render-2"); parser_ctxt = xmlNewParserCtxt(); CRM_ASSERT(sfile != NULL); pcmk__mem_assert(parser_ctxt); xslt_doc = xmlCtxtReadFile(parser_ctxt, sfile, NULL, XML_PARSE_NONET); xslt = xsltParseStylesheetDoc(xslt_doc); /* acquires xslt_doc! */ if (xslt == NULL) { crm_crit("Problem in parsing %s", sfile); rc = EINVAL; goto done; } xmlFreeParserCtxt(parser_ctxt); xslt_ctxt = xsltNewTransformContext(xslt, annotated_doc); pcmk__mem_assert(xslt_ctxt); switch (how) { case pcmk__acl_render_namespace: params = params_namespace; break; case pcmk__acl_render_text: params = params_noansi; break; default: /* pcmk__acl_render_color is the only remaining option. * The compiler complains about params possibly uninitialized if we * don't use default here. */ params = params_useansi; break; } xsltQuoteUserParams(xslt_ctxt, params); res = xsltApplyStylesheetUser(xslt, annotated_doc, NULL, NULL, NULL, xslt_ctxt); - xmlFreeDoc(annotated_doc); + pcmk__xml_free_doc(annotated_doc); annotated_doc = NULL; xsltFreeTransformContext(xslt_ctxt); xslt_ctxt = NULL; if (how == pcmk__acl_render_color && params != params_useansi) { char **param_i = (char **) params; do { free(*param_i); } while (*param_i++ != NULL); free(params); } if (res == NULL) { rc = EINVAL; } else { int doc_txt_len; int temp = xsltSaveResultToString(doc_txt_ptr, &doc_txt_len, res, xslt); - xmlFreeDoc(res); + + pcmk__xml_free_doc(res); if (temp != 0) { rc = EINVAL; } } done: if (xslt != NULL) { xsltFreeStylesheet(xslt); } free(sfile); return rc; } diff --git a/lib/pacemaker/pcmk_output.c b/lib/pacemaker/pcmk_output.c index e682f108a1..cb16d3d271 100644 --- a/lib/pacemaker/pcmk_output.c +++ b/lib/pacemaker/pcmk_output.c @@ -1,2715 +1,2711 @@ /* * Copyright 2019-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include // stonith__* #include #include #include #include #include static char * colocations_header(pcmk_resource_t *rsc, pcmk__colocation_t *cons, bool dependents) { char *retval = NULL; if (cons->primary_role > pcmk_role_started) { retval = crm_strdup_printf("%s (score=%s, %s role=%s, id=%s)", rsc->id, pcmk_readable_score(cons->score), (dependents? "needs" : "with"), pcmk_role_text(cons->primary_role), cons->id); } else { retval = crm_strdup_printf("%s (score=%s, id=%s)", rsc->id, pcmk_readable_score(cons->score), cons->id); } return retval; } static void colocations_xml_node(pcmk__output_t *out, pcmk_resource_t *rsc, pcmk__colocation_t *cons) { - xmlNodePtr node = NULL; - - node = pcmk__output_create_xml_node(out, PCMK_XE_RSC_COLOCATION, - PCMK_XA_ID, cons->id, - PCMK_XA_RSC, cons->dependent->id, - PCMK_XA_WITH_RSC, cons->primary->id, - PCMK_XA_SCORE, - pcmk_readable_score(cons->score), - NULL); - - if (cons->node_attribute) { - xmlSetProp(node, (pcmkXmlStr) PCMK_XA_NODE_ATTRIBUTE, - (pcmkXmlStr) cons->node_attribute); - } + const char *score = pcmk_readable_score(cons->score); + const char *dependent_role = NULL; + const char *primary_role = NULL; if (cons->dependent_role != pcmk_role_unknown) { - xmlSetProp(node, (pcmkXmlStr) PCMK_XA_RSC_ROLE, - (pcmkXmlStr) pcmk_role_text(cons->dependent_role)); + dependent_role = pcmk_role_text(cons->dependent_role); } - if (cons->primary_role != pcmk_role_unknown) { - xmlSetProp(node, (pcmkXmlStr) PCMK_XA_WITH_RSC_ROLE, - (pcmkXmlStr) pcmk_role_text(cons->primary_role)); + primary_role = pcmk_role_text(cons->primary_role); } + + pcmk__output_create_xml_node(out, PCMK_XE_RSC_COLOCATION, + PCMK_XA_ID, cons->id, + PCMK_XA_RSC, cons->dependent->id, + PCMK_XA_WITH_RSC, cons->primary->id, + PCMK_XA_SCORE, score, + PCMK_XA_NODE_ATTRIBUTE, cons->node_attribute, + PCMK_XA_RSC_ROLE, dependent_role, + PCMK_XA_WITH_RSC_ROLE, primary_role, + NULL); } static int do_locations_list_xml(pcmk__output_t *out, pcmk_resource_t *rsc, bool add_header) { GList *lpc = NULL; int rc = pcmk_rc_no_output; for (lpc = rsc->priv->location_constraints; lpc != NULL; lpc = lpc->next) { pcmk__location_t *cons = lpc->data; GList *lpc2 = NULL; for (lpc2 = cons->nodes; lpc2 != NULL; lpc2 = lpc2->next) { pcmk_node_t *node = (pcmk_node_t *) lpc2->data; if (add_header) { PCMK__OUTPUT_LIST_HEADER(out, false, rc, "locations"); } pcmk__output_create_xml_node(out, PCMK_XE_RSC_LOCATION, PCMK_XA_NODE, node->priv->name, PCMK_XA_RSC, rsc->id, PCMK_XA_ID, cons->id, PCMK_XA_SCORE, pcmk_readable_score(node->assign->score), NULL); } } if (add_header) { PCMK__OUTPUT_LIST_FOOTER(out, rc); } return rc; } PCMK__OUTPUT_ARGS("rsc-action-item", "const char *", "pcmk_resource_t *", "pcmk_node_t *", "pcmk_node_t *", "pcmk_action_t *", "pcmk_action_t *") static int rsc_action_item(pcmk__output_t *out, va_list args) { const char *change = va_arg(args, const char *); pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *); pcmk_node_t *origin = va_arg(args, pcmk_node_t *); pcmk_node_t *destination = va_arg(args, pcmk_node_t *); pcmk_action_t *action = va_arg(args, pcmk_action_t *); pcmk_action_t *source = va_arg(args, pcmk_action_t *); int len = 0; char *reason = NULL; char *details = NULL; bool same_host = false; bool same_role = false; bool need_role = false; static int rsc_width = 5; static int detail_width = 5; CRM_ASSERT(action); CRM_ASSERT(destination != NULL || origin != NULL); if (source == NULL) { source = action; } len = strlen(rsc->id); if (len > rsc_width) { rsc_width = len + 2; } if ((rsc->priv->orig_role > pcmk_role_started) || (rsc->priv->next_role > pcmk_role_unpromoted)) { need_role = true; } if (pcmk__same_node(origin, destination)) { same_host = true; } if (rsc->priv->orig_role == rsc->priv->next_role) { same_role = true; } if (need_role && (origin == NULL)) { /* Starting and promoting a promotable clone instance */ details = crm_strdup_printf("%s -> %s %s", pcmk_role_text(rsc->priv->orig_role), pcmk_role_text(rsc->priv->next_role), pcmk__node_name(destination)); } else if (origin == NULL) { /* Starting a resource */ details = crm_strdup_printf("%s", pcmk__node_name(destination)); } else if (need_role && (destination == NULL)) { /* Stopping a promotable clone instance */ details = crm_strdup_printf("%s %s", pcmk_role_text(rsc->priv->orig_role), pcmk__node_name(origin)); } else if (destination == NULL) { /* Stopping a resource */ details = crm_strdup_printf("%s", pcmk__node_name(origin)); } else if (need_role && same_role && same_host) { /* Recovering, restarting or re-promoting a promotable clone instance */ details = crm_strdup_printf("%s %s", pcmk_role_text(rsc->priv->orig_role), pcmk__node_name(origin)); } else if (same_role && same_host) { /* Recovering or Restarting a normal resource */ details = crm_strdup_printf("%s", pcmk__node_name(origin)); } else if (need_role && same_role) { /* Moving a promotable clone instance */ details = crm_strdup_printf("%s -> %s %s", pcmk__node_name(origin), pcmk__node_name(destination), pcmk_role_text(rsc->priv->orig_role)); } else if (same_role) { /* Moving a normal resource */ details = crm_strdup_printf("%s -> %s", pcmk__node_name(origin), pcmk__node_name(destination)); } else if (same_host) { /* Promoting or demoting a promotable clone instance */ details = crm_strdup_printf("%s -> %s %s", pcmk_role_text(rsc->priv->orig_role), pcmk_role_text(rsc->priv->next_role), pcmk__node_name(origin)); } else { /* Moving and promoting/demoting */ details = crm_strdup_printf("%s %s -> %s %s", pcmk_role_text(rsc->priv->orig_role), pcmk__node_name(origin), pcmk_role_text(rsc->priv->next_role), pcmk__node_name(destination)); } len = strlen(details); if (len > detail_width) { detail_width = len; } if ((source->reason != NULL) && !pcmk_is_set(action->flags, pcmk__action_runnable)) { reason = crm_strdup_printf("due to %s (blocked)", source->reason); } else if (source->reason) { reason = crm_strdup_printf("due to %s", source->reason); } else if (!pcmk_is_set(action->flags, pcmk__action_runnable)) { reason = strdup("blocked"); } out->list_item(out, NULL, "%-8s %-*s ( %*s )%s%s", change, rsc_width, rsc->id, detail_width, details, ((reason == NULL)? "" : " "), pcmk__s(reason, "")); free(details); free(reason); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("rsc-action-item", "const char *", "pcmk_resource_t *", "pcmk_node_t *", "pcmk_node_t *", "pcmk_action_t *", "pcmk_action_t *") static int rsc_action_item_xml(pcmk__output_t *out, va_list args) { const char *change = va_arg(args, const char *); pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *); pcmk_node_t *origin = va_arg(args, pcmk_node_t *); pcmk_node_t *destination = va_arg(args, pcmk_node_t *); pcmk_action_t *action = va_arg(args, pcmk_action_t *); pcmk_action_t *source = va_arg(args, pcmk_action_t *); char *change_str = NULL; bool same_host = false; bool same_role = false; bool need_role = false; xmlNode *xml = NULL; CRM_ASSERT(action); CRM_ASSERT(destination != NULL || origin != NULL); if (source == NULL) { source = action; } if ((rsc->priv->orig_role > pcmk_role_started) || (rsc->priv->next_role > pcmk_role_unpromoted)) { need_role = true; } if (pcmk__same_node(origin, destination)) { same_host = true; } if (rsc->priv->orig_role == rsc->priv->next_role) { same_role = true; } change_str = g_ascii_strdown(change, -1); xml = pcmk__output_create_xml_node(out, PCMK_XE_RSC_ACTION, PCMK_XA_ACTION, change_str, PCMK_XA_RESOURCE, rsc->id, NULL); g_free(change_str); if (need_role && (origin == NULL)) { /* Starting and promoting a promotable clone instance */ pcmk__xe_set_props(xml, PCMK_XA_ROLE, pcmk_role_text(rsc->priv->orig_role), PCMK_XA_NEXT_ROLE, pcmk_role_text(rsc->priv->next_role), PCMK_XA_DEST, destination->priv->name, NULL); } else if (origin == NULL) { /* Starting a resource */ crm_xml_add(xml, PCMK_XA_NODE, destination->priv->name); } else if (need_role && (destination == NULL)) { /* Stopping a promotable clone instance */ pcmk__xe_set_props(xml, PCMK_XA_ROLE, pcmk_role_text(rsc->priv->orig_role), PCMK_XA_NODE, origin->priv->name, NULL); } else if (destination == NULL) { /* Stopping a resource */ crm_xml_add(xml, PCMK_XA_NODE, origin->priv->name); } else if (need_role && same_role && same_host) { /* Recovering, restarting or re-promoting a promotable clone instance */ pcmk__xe_set_props(xml, PCMK_XA_ROLE, pcmk_role_text(rsc->priv->orig_role), PCMK_XA_SOURCE, origin->priv->name, NULL); } else if (same_role && same_host) { /* Recovering or Restarting a normal resource */ crm_xml_add(xml, PCMK_XA_SOURCE, origin->priv->name); } else if (need_role && same_role) { /* Moving a promotable clone instance */ pcmk__xe_set_props(xml, PCMK_XA_SOURCE, origin->priv->name, PCMK_XA_DEST, destination->priv->name, PCMK_XA_ROLE, pcmk_role_text(rsc->priv->orig_role), NULL); } else if (same_role) { /* Moving a normal resource */ pcmk__xe_set_props(xml, PCMK_XA_SOURCE, origin->priv->name, PCMK_XA_DEST, destination->priv->name, NULL); } else if (same_host) { /* Promoting or demoting a promotable clone instance */ pcmk__xe_set_props(xml, PCMK_XA_ROLE, pcmk_role_text(rsc->priv->orig_role), PCMK_XA_NEXT_ROLE, pcmk_role_text(rsc->priv->next_role), PCMK_XA_SOURCE, origin->priv->name, NULL); } else { /* Moving and promoting/demoting */ pcmk__xe_set_props(xml, PCMK_XA_ROLE, pcmk_role_text(rsc->priv->orig_role), PCMK_XA_SOURCE, origin->priv->name, PCMK_XA_NEXT_ROLE, pcmk_role_text(rsc->priv->next_role), PCMK_XA_DEST, destination->priv->name, NULL); } if ((source->reason != NULL) && !pcmk_is_set(action->flags, pcmk__action_runnable)) { pcmk__xe_set_props(xml, PCMK_XA_REASON, source->reason, PCMK_XA_BLOCKED, PCMK_VALUE_TRUE, NULL); } else if (source->reason != NULL) { crm_xml_add(xml, PCMK_XA_REASON, source->reason); } else if (!pcmk_is_set(action->flags, pcmk__action_runnable)) { pcmk__xe_set_bool_attr(xml, PCMK_XA_BLOCKED, true); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("rsc-is-colocated-with-list", "pcmk_resource_t *", "bool") static int rsc_is_colocated_with_list(pcmk__output_t *out, va_list args) { pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *); bool recursive = va_arg(args, int); int rc = pcmk_rc_no_output; if (pcmk_is_set(rsc->flags, pcmk__rsc_detect_loop)) { return rc; } /* We're listing constraints explicitly involving rsc, so use * rsc->private->this_with_colocations directly rather than call * rsc->private->cmds->this_with_colocations(). */ pcmk__set_rsc_flags(rsc, pcmk__rsc_detect_loop); for (GList *lpc = rsc->priv->this_with_colocations; lpc != NULL; lpc = lpc->next) { pcmk__colocation_t *cons = (pcmk__colocation_t *) lpc->data; char *hdr = NULL; PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Resources %s is colocated with", rsc->id); if (pcmk_is_set(cons->primary->flags, pcmk__rsc_detect_loop)) { out->list_item(out, NULL, "%s (id=%s - loop)", cons->primary->id, cons->id); continue; } hdr = colocations_header(cons->primary, cons, false); out->list_item(out, NULL, "%s", hdr); free(hdr); // Empty list header for indentation of information about this resource out->begin_list(out, NULL, NULL, NULL); out->message(out, "locations-list", cons->primary); if (recursive) { out->message(out, "rsc-is-colocated-with-list", cons->primary, recursive); } out->end_list(out); } PCMK__OUTPUT_LIST_FOOTER(out, rc); return rc; } PCMK__OUTPUT_ARGS("rsc-is-colocated-with-list", "pcmk_resource_t *", "bool") static int rsc_is_colocated_with_list_xml(pcmk__output_t *out, va_list args) { pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *); bool recursive = va_arg(args, int); int rc = pcmk_rc_no_output; if (pcmk_is_set(rsc->flags, pcmk__rsc_detect_loop)) { return rc; } /* We're listing constraints explicitly involving rsc, so use * rsc->private->this_with_colocations directly rather than call * rsc->private->cmds->this_with_colocations(). */ pcmk__set_rsc_flags(rsc, pcmk__rsc_detect_loop); for (GList *lpc = rsc->priv->this_with_colocations; lpc != NULL; lpc = lpc->next) { pcmk__colocation_t *cons = (pcmk__colocation_t *) lpc->data; if (pcmk_is_set(cons->primary->flags, pcmk__rsc_detect_loop)) { colocations_xml_node(out, cons->primary, cons); continue; } colocations_xml_node(out, cons->primary, cons); do_locations_list_xml(out, cons->primary, false); if (recursive) { out->message(out, "rsc-is-colocated-with-list", cons->primary, recursive); } } return rc; } PCMK__OUTPUT_ARGS("rscs-colocated-with-list", "pcmk_resource_t *", "bool") static int rscs_colocated_with_list(pcmk__output_t *out, va_list args) { pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *); bool recursive = va_arg(args, int); int rc = pcmk_rc_no_output; if (pcmk_is_set(rsc->flags, pcmk__rsc_detect_loop)) { return rc; } /* We're listing constraints explicitly involving rsc, so use * rsc->private->with_this_colocations directly rather than * rsc->private->cmds->with_this_colocations(). */ pcmk__set_rsc_flags(rsc, pcmk__rsc_detect_loop); for (GList *lpc = rsc->priv->with_this_colocations; lpc != NULL; lpc = lpc->next) { pcmk__colocation_t *cons = (pcmk__colocation_t *) lpc->data; char *hdr = NULL; PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Resources colocated with %s", rsc->id); if (pcmk_is_set(cons->dependent->flags, pcmk__rsc_detect_loop)) { out->list_item(out, NULL, "%s (id=%s - loop)", cons->dependent->id, cons->id); continue; } hdr = colocations_header(cons->dependent, cons, true); out->list_item(out, NULL, "%s", hdr); free(hdr); // Empty list header for indentation of information about this resource out->begin_list(out, NULL, NULL, NULL); out->message(out, "locations-list", cons->dependent); if (recursive) { out->message(out, "rscs-colocated-with-list", cons->dependent, recursive); } out->end_list(out); } PCMK__OUTPUT_LIST_FOOTER(out, rc); return rc; } PCMK__OUTPUT_ARGS("rscs-colocated-with-list", "pcmk_resource_t *", "bool") static int rscs_colocated_with_list_xml(pcmk__output_t *out, va_list args) { pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *); bool recursive = va_arg(args, int); int rc = pcmk_rc_no_output; if (pcmk_is_set(rsc->flags, pcmk__rsc_detect_loop)) { return rc; } /* We're listing constraints explicitly involving rsc, so use * rsc->private->with_this_colocations directly rather than * rsc->private->cmds->with_this_colocations(). */ pcmk__set_rsc_flags(rsc, pcmk__rsc_detect_loop); for (GList *lpc = rsc->priv->with_this_colocations; lpc != NULL; lpc = lpc->next) { pcmk__colocation_t *cons = (pcmk__colocation_t *) lpc->data; if (pcmk_is_set(cons->dependent->flags, pcmk__rsc_detect_loop)) { colocations_xml_node(out, cons->dependent, cons); continue; } colocations_xml_node(out, cons->dependent, cons); do_locations_list_xml(out, cons->dependent, false); if (recursive) { out->message(out, "rscs-colocated-with-list", cons->dependent, recursive); } } return rc; } PCMK__OUTPUT_ARGS("locations-list", "pcmk_resource_t *") static int locations_list(pcmk__output_t *out, va_list args) { pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *); GList *lpc = NULL; int rc = pcmk_rc_no_output; for (lpc = rsc->priv->location_constraints; lpc != NULL; lpc = lpc->next) { pcmk__location_t *cons = lpc->data; GList *lpc2 = NULL; for (lpc2 = cons->nodes; lpc2 != NULL; lpc2 = lpc2->next) { pcmk_node_t *node = (pcmk_node_t *) lpc2->data; PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Locations"); out->list_item(out, NULL, "Node %s (score=%s, id=%s, rsc=%s)", pcmk__node_name(node), pcmk_readable_score(node->assign->score), cons->id, rsc->id); } } PCMK__OUTPUT_LIST_FOOTER(out, rc); return rc; } PCMK__OUTPUT_ARGS("locations-list", "pcmk_resource_t *") static int locations_list_xml(pcmk__output_t *out, va_list args) { pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *); return do_locations_list_xml(out, rsc, true); } PCMK__OUTPUT_ARGS("locations-and-colocations", "pcmk_resource_t *", "bool", "bool") static int locations_and_colocations(pcmk__output_t *out, va_list args) { pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *); bool recursive = va_arg(args, int); bool force = va_arg(args, int); pcmk__unpack_constraints(rsc->priv->scheduler); // Constraints apply to group/clone, not member/instance if (!force) { rsc = uber_parent(rsc); } out->message(out, "locations-list", rsc); pe__clear_resource_flags_on_all(rsc->priv->scheduler, pcmk__rsc_detect_loop); out->message(out, "rscs-colocated-with-list", rsc, recursive); pe__clear_resource_flags_on_all(rsc->priv->scheduler, pcmk__rsc_detect_loop); out->message(out, "rsc-is-colocated-with-list", rsc, recursive); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("locations-and-colocations", "pcmk_resource_t *", "bool", "bool") static int locations_and_colocations_xml(pcmk__output_t *out, va_list args) { pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *); bool recursive = va_arg(args, int); bool force = va_arg(args, int); pcmk__unpack_constraints(rsc->priv->scheduler); // Constraints apply to group/clone, not member/instance if (!force) { rsc = uber_parent(rsc); } pcmk__output_xml_create_parent(out, PCMK_XE_CONSTRAINTS, NULL); do_locations_list_xml(out, rsc, false); pe__clear_resource_flags_on_all(rsc->priv->scheduler, pcmk__rsc_detect_loop); out->message(out, "rscs-colocated-with-list", rsc, recursive); pe__clear_resource_flags_on_all(rsc->priv->scheduler, pcmk__rsc_detect_loop); out->message(out, "rsc-is-colocated-with-list", rsc, recursive); pcmk__output_xml_pop_parent(out); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("health", "const char *", "const char *", "const char *", "const char *") static int health(pcmk__output_t *out, va_list args) { const char *sys_from G_GNUC_UNUSED = va_arg(args, const char *); const char *host_from = va_arg(args, const char *); const char *fsa_state = va_arg(args, const char *); const char *result = va_arg(args, const char *); return out->info(out, "Controller on %s in state %s: %s", pcmk__s(host_from, "unknown node"), pcmk__s(fsa_state, "unknown"), pcmk__s(result, "unknown result")); } PCMK__OUTPUT_ARGS("health", "const char *", "const char *", "const char *", "const char *") static int health_text(pcmk__output_t *out, va_list args) { if (!out->is_quiet(out)) { return health(out, args); } else { const char *sys_from G_GNUC_UNUSED = va_arg(args, const char *); const char *host_from G_GNUC_UNUSED = va_arg(args, const char *); const char *fsa_state = va_arg(args, const char *); const char *result G_GNUC_UNUSED = va_arg(args, const char *); if (fsa_state != NULL) { pcmk__formatted_printf(out, "%s\n", fsa_state); return pcmk_rc_ok; } } return pcmk_rc_no_output; } PCMK__OUTPUT_ARGS("health", "const char *", "const char *", "const char *", "const char *") static int health_xml(pcmk__output_t *out, va_list args) { const char *sys_from = va_arg(args, const char *); const char *host_from = va_arg(args, const char *); const char *fsa_state = va_arg(args, const char *); const char *result = va_arg(args, const char *); pcmk__output_create_xml_node(out, pcmk__s(sys_from, ""), PCMK_XA_NODE_NAME, pcmk__s(host_from, ""), PCMK_XA_STATE, pcmk__s(fsa_state, ""), PCMK_XA_RESULT, pcmk__s(result, ""), NULL); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("pacemakerd-health", "const char *", "enum pcmk_pacemakerd_state", "const char *", "time_t") static int pacemakerd_health(pcmk__output_t *out, va_list args) { const char *sys_from = va_arg(args, const char *); enum pcmk_pacemakerd_state state = (enum pcmk_pacemakerd_state) va_arg(args, int); const char *state_s = va_arg(args, const char *); time_t last_updated = va_arg(args, time_t); char *last_updated_s = NULL; int rc = pcmk_rc_ok; if (sys_from == NULL) { if (state == pcmk_pacemakerd_state_remote) { sys_from = PCMK__SERVER_REMOTED; } else { sys_from = CRM_SYSTEM_MCP; } } if (state_s == NULL) { state_s = pcmk__pcmkd_state_enum2friendly(state); } if (last_updated != 0) { last_updated_s = pcmk__epoch2str(&last_updated, crm_time_log_date |crm_time_log_timeofday |crm_time_log_with_timezone); } rc = out->info(out, "Status of %s: '%s' (last updated %s)", sys_from, state_s, pcmk__s(last_updated_s, "at unknown time")); free(last_updated_s); return rc; } PCMK__OUTPUT_ARGS("pacemakerd-health", "const char *", "enum pcmk_pacemakerd_state", "const char *", "time_t") static int pacemakerd_health_html(pcmk__output_t *out, va_list args) { const char *sys_from = va_arg(args, const char *); enum pcmk_pacemakerd_state state = (enum pcmk_pacemakerd_state) va_arg(args, int); const char *state_s = va_arg(args, const char *); time_t last_updated = va_arg(args, time_t); char *last_updated_s = NULL; char *msg = NULL; if (sys_from == NULL) { if (state == pcmk_pacemakerd_state_remote) { sys_from = PCMK__SERVER_REMOTED; } else { sys_from = CRM_SYSTEM_MCP; } } if (state_s == NULL) { state_s = pcmk__pcmkd_state_enum2friendly(state); } if (last_updated != 0) { last_updated_s = pcmk__epoch2str(&last_updated, crm_time_log_date |crm_time_log_timeofday |crm_time_log_with_timezone); } msg = crm_strdup_printf("Status of %s: '%s' (last updated %s)", sys_from, state_s, pcmk__s(last_updated_s, "at unknown time")); pcmk__output_create_html_node(out, "li", NULL, NULL, msg); free(msg); free(last_updated_s); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("pacemakerd-health", "const char *", "enum pcmk_pacemakerd_state", "const char *", "time_t") static int pacemakerd_health_text(pcmk__output_t *out, va_list args) { if (!out->is_quiet(out)) { return pacemakerd_health(out, args); } else { const char *sys_from G_GNUC_UNUSED = va_arg(args, const char *); enum pcmk_pacemakerd_state state = (enum pcmk_pacemakerd_state) va_arg(args, int); const char *state_s = va_arg(args, const char *); time_t last_updated G_GNUC_UNUSED = va_arg(args, time_t); if (state_s == NULL) { state_s = pcmk_pacemakerd_api_daemon_state_enum2text(state); } pcmk__formatted_printf(out, "%s\n", state_s); return pcmk_rc_ok; } } PCMK__OUTPUT_ARGS("pacemakerd-health", "const char *", "enum pcmk_pacemakerd_state", "const char *", "time_t") static int pacemakerd_health_xml(pcmk__output_t *out, va_list args) { const char *sys_from = va_arg(args, const char *); enum pcmk_pacemakerd_state state = (enum pcmk_pacemakerd_state) va_arg(args, int); const char *state_s = va_arg(args, const char *); time_t last_updated = va_arg(args, time_t); char *last_updated_s = NULL; if (sys_from == NULL) { if (state == pcmk_pacemakerd_state_remote) { sys_from = PCMK__SERVER_REMOTED; } else { sys_from = CRM_SYSTEM_MCP; } } if (state_s == NULL) { state_s = pcmk_pacemakerd_api_daemon_state_enum2text(state); } if (last_updated != 0) { last_updated_s = pcmk__epoch2str(&last_updated, crm_time_log_date |crm_time_log_timeofday |crm_time_log_with_timezone); } pcmk__output_create_xml_node(out, PCMK_XE_PACEMAKERD, PCMK_XA_SYS_FROM, sys_from, PCMK_XA_STATE, state_s, PCMK_XA_LAST_UPDATED, last_updated_s, NULL); free(last_updated_s); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("profile", "const char *", "clock_t", "clock_t") static int profile_default(pcmk__output_t *out, va_list args) { const char *xml_file = va_arg(args, const char *); clock_t start = va_arg(args, clock_t); clock_t end = va_arg(args, clock_t); out->list_item(out, NULL, "Testing %s ... %.2f secs", xml_file, (end - start) / (float) CLOCKS_PER_SEC); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("profile", "const char *", "clock_t", "clock_t") static int profile_xml(pcmk__output_t *out, va_list args) { const char *xml_file = va_arg(args, const char *); clock_t start = va_arg(args, clock_t); clock_t end = va_arg(args, clock_t); char *duration = pcmk__ftoa((end - start) / (float) CLOCKS_PER_SEC); pcmk__output_create_xml_node(out, PCMK_XE_TIMING, PCMK_XA_FILE, xml_file, PCMK_XA_DURATION, duration, NULL); free(duration); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("dc", "const char *") static int dc(pcmk__output_t *out, va_list args) { const char *dc = va_arg(args, const char *); return out->info(out, "Designated Controller is: %s", pcmk__s(dc, "not yet elected")); } PCMK__OUTPUT_ARGS("dc", "const char *") static int dc_text(pcmk__output_t *out, va_list args) { if (!out->is_quiet(out)) { return dc(out, args); } else { const char *dc = va_arg(args, const char *); if (dc != NULL) { pcmk__formatted_printf(out, "%s\n", pcmk__s(dc, "")); return pcmk_rc_ok; } } return pcmk_rc_no_output; } PCMK__OUTPUT_ARGS("dc", "const char *") static int dc_xml(pcmk__output_t *out, va_list args) { const char *dc = va_arg(args, const char *); pcmk__output_create_xml_node(out, PCMK_XE_DC, PCMK_XA_NODE_NAME, pcmk__s(dc, ""), NULL); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("crmadmin-node", "const char *", "const char *", "const char *", "bool") static int crmadmin_node(pcmk__output_t *out, va_list args) { const char *type = va_arg(args, const char *); const char *name = va_arg(args, const char *); const char *id = va_arg(args, const char *); bool bash_export = va_arg(args, int); if (bash_export) { return out->info(out, "export %s=%s", pcmk__s(name, ""), pcmk__s(id, "")); } else { return out->info(out, "%s node: %s (%s)", type ? type : "cluster", pcmk__s(name, ""), pcmk__s(id, "")); } } PCMK__OUTPUT_ARGS("crmadmin-node", "const char *", "const char *", "const char *", "bool") static int crmadmin_node_text(pcmk__output_t *out, va_list args) { if (!out->is_quiet(out)) { return crmadmin_node(out, args); } else { const char *type G_GNUC_UNUSED = va_arg(args, const char *); const char *name = va_arg(args, const char *); const char *id G_GNUC_UNUSED = va_arg(args, const char *); bool bash_export G_GNUC_UNUSED = va_arg(args, int); pcmk__formatted_printf(out, "%s\n", pcmk__s(name, "")); return pcmk_rc_ok; } } PCMK__OUTPUT_ARGS("crmadmin-node", "const char *", "const char *", "const char *", "bool") static int crmadmin_node_xml(pcmk__output_t *out, va_list args) { const char *type = va_arg(args, const char *); const char *name = va_arg(args, const char *); const char *id = va_arg(args, const char *); bool bash_export G_GNUC_UNUSED = va_arg(args, int); pcmk__output_create_xml_node(out, PCMK_XE_NODE, PCMK_XA_TYPE, pcmk__s(type, "cluster"), PCMK_XA_NAME, pcmk__s(name, ""), PCMK_XA_ID, pcmk__s(id, ""), NULL); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("digests", "const pcmk_resource_t *", "const pcmk_node_t *", "const char *", "guint", "const pcmk__op_digest_t *") static int digests_text(pcmk__output_t *out, va_list args) { const pcmk_resource_t *rsc = va_arg(args, const pcmk_resource_t *); const pcmk_node_t *node = va_arg(args, const pcmk_node_t *); const char *task = va_arg(args, const char *); guint interval_ms = va_arg(args, guint); const pcmk__op_digest_t *digests = va_arg(args, const pcmk__op_digest_t *); char *action_desc = NULL; const char *rsc_desc = "unknown resource"; const char *node_desc = "unknown node"; if (interval_ms != 0) { action_desc = crm_strdup_printf("%ums-interval %s action", interval_ms, ((task == NULL)? "unknown" : task)); } else if (pcmk__str_eq(task, PCMK_ACTION_MONITOR, pcmk__str_none)) { action_desc = strdup("probe action"); } else { action_desc = crm_strdup_printf("%s action", ((task == NULL)? "unknown" : task)); } if ((rsc != NULL) && (rsc->id != NULL)) { rsc_desc = rsc->id; } if ((node != NULL) && (node->priv->name != NULL)) { node_desc = node->priv->name; } out->begin_list(out, NULL, NULL, "Digests for %s %s on %s", rsc_desc, action_desc, node_desc); free(action_desc); if (digests == NULL) { out->list_item(out, NULL, "none"); out->end_list(out); return pcmk_rc_ok; } if (digests->digest_all_calc != NULL) { out->list_item(out, NULL, "%s (all parameters)", digests->digest_all_calc); } if (digests->digest_secure_calc != NULL) { out->list_item(out, NULL, "%s (non-private parameters)", digests->digest_secure_calc); } if (digests->digest_restart_calc != NULL) { out->list_item(out, NULL, "%s (non-reloadable parameters)", digests->digest_restart_calc); } out->end_list(out); return pcmk_rc_ok; } static void add_digest_xml(xmlNode *parent, const char *type, const char *digest, xmlNode *digest_source) { if (digest != NULL) { xmlNodePtr digest_xml = pcmk__xe_create(parent, PCMK_XE_DIGEST); crm_xml_add(digest_xml, PCMK_XA_TYPE, pcmk__s(type, "unspecified")); crm_xml_add(digest_xml, PCMK_XA_HASH, digest); pcmk__xml_copy(digest_xml, digest_source); } } PCMK__OUTPUT_ARGS("digests", "const pcmk_resource_t *", "const pcmk_node_t *", "const char *", "guint", "const pcmk__op_digest_t *") static int digests_xml(pcmk__output_t *out, va_list args) { const pcmk_resource_t *rsc = va_arg(args, const pcmk_resource_t *); const pcmk_node_t *node = va_arg(args, const pcmk_node_t *); const char *task = va_arg(args, const char *); guint interval_ms = va_arg(args, guint); const pcmk__op_digest_t *digests = va_arg(args, const pcmk__op_digest_t *); char *interval_s = crm_strdup_printf("%ums", interval_ms); xmlNode *xml = NULL; xml = pcmk__output_create_xml_node(out, PCMK_XE_DIGESTS, PCMK_XA_RESOURCE, pcmk__s(rsc->id, ""), PCMK_XA_NODE, pcmk__s(node->priv->name, ""), PCMK_XA_TASK, pcmk__s(task, ""), PCMK_XA_INTERVAL, interval_s, NULL); free(interval_s); if (digests != NULL) { add_digest_xml(xml, "all", digests->digest_all_calc, digests->params_all); add_digest_xml(xml, "nonprivate", digests->digest_secure_calc, digests->params_secure); add_digest_xml(xml, "nonreloadable", digests->digest_restart_calc, digests->params_restart); } return pcmk_rc_ok; } #define STOP_SANITY_ASSERT(lineno) do { \ if ((current != NULL) && current->details->unclean) { \ /* It will be a pseudo op */ \ } else if (stop == NULL) { \ crm_err("%s:%d: No stop action exists for %s", \ __func__, lineno, rsc->id); \ CRM_ASSERT(stop != NULL); \ } else if (pcmk_is_set(stop->flags, pcmk__action_optional)) { \ crm_err("%s:%d: Action %s is still optional", \ __func__, lineno, stop->uuid); \ CRM_ASSERT(!pcmk_is_set(stop->flags, pcmk__action_optional)); \ } \ } while (0) PCMK__OUTPUT_ARGS("rsc-action", "pcmk_resource_t *", "pcmk_node_t *", "pcmk_node_t *") static int rsc_action_default(pcmk__output_t *out, va_list args) { pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *); pcmk_node_t *current = va_arg(args, pcmk_node_t *); pcmk_node_t *next = va_arg(args, pcmk_node_t *); GList *possible_matches = NULL; char *key = NULL; int rc = pcmk_rc_no_output; bool moving = false; pcmk_node_t *start_node = NULL; pcmk_action_t *start = NULL; pcmk_action_t *stop = NULL; pcmk_action_t *promote = NULL; pcmk_action_t *demote = NULL; pcmk_action_t *reason_op = NULL; if (!pcmk_is_set(rsc->flags, pcmk__rsc_managed) || (current == NULL && next == NULL)) { const bool managed = pcmk_is_set(rsc->flags, pcmk__rsc_managed); pcmk__rsc_info(rsc, "Leave %s\t(%s%s)", rsc->id, pcmk_role_text(rsc->priv->orig_role), (managed? "" : " unmanaged")); return rc; } moving = (current != NULL) && (next != NULL) && !pcmk__same_node(current, next); possible_matches = pe__resource_actions(rsc, next, PCMK_ACTION_START, false); if (possible_matches) { start = possible_matches->data; g_list_free(possible_matches); } if ((start == NULL) || !pcmk_is_set(start->flags, pcmk__action_runnable)) { start_node = NULL; } else { start_node = current; } possible_matches = pe__resource_actions(rsc, start_node, PCMK_ACTION_STOP, false); if (possible_matches) { stop = possible_matches->data; g_list_free(possible_matches); } else if (pcmk_is_set(rsc->flags, pcmk__rsc_stop_unexpected)) { /* The resource is multiply active with PCMK_META_MULTIPLE_ACTIVE set to * PCMK_VALUE_STOP_UNEXPECTED, and not stopping on its current node, but * it should be stopping elsewhere. */ possible_matches = pe__resource_actions(rsc, NULL, PCMK_ACTION_STOP, false); if (possible_matches != NULL) { stop = possible_matches->data; g_list_free(possible_matches); } } possible_matches = pe__resource_actions(rsc, next, PCMK_ACTION_PROMOTE, false); if (possible_matches) { promote = possible_matches->data; g_list_free(possible_matches); } possible_matches = pe__resource_actions(rsc, next, PCMK_ACTION_DEMOTE, false); if (possible_matches) { demote = possible_matches->data; g_list_free(possible_matches); } if (rsc->priv->orig_role == rsc->priv->next_role) { pcmk_action_t *migrate_op = NULL; CRM_CHECK(next != NULL, return rc); possible_matches = pe__resource_actions(rsc, next, PCMK_ACTION_MIGRATE_FROM, false); if (possible_matches) { migrate_op = possible_matches->data; } if ((migrate_op != NULL) && (current != NULL) && pcmk_is_set(migrate_op->flags, pcmk__action_runnable)) { rc = out->message(out, "rsc-action-item", "Migrate", rsc, current, next, start, NULL); } else if (pcmk_is_set(rsc->flags, pcmk__rsc_reload)) { rc = out->message(out, "rsc-action-item", "Reload", rsc, current, next, start, NULL); } else if ((start == NULL) || pcmk_is_set(start->flags, pcmk__action_optional)) { if ((demote != NULL) && (promote != NULL) && !pcmk_is_set(demote->flags, pcmk__action_optional) && !pcmk_is_set(promote->flags, pcmk__action_optional)) { rc = out->message(out, "rsc-action-item", "Re-promote", rsc, current, next, promote, demote); } else { pcmk__rsc_info(rsc, "Leave %s\t(%s %s)", rsc->id, pcmk_role_text(rsc->priv->orig_role), pcmk__node_name(next)); } } else if (!pcmk_is_set(start->flags, pcmk__action_runnable)) { if ((stop == NULL) || (stop->reason == NULL)) { reason_op = start; } else { reason_op = stop; } rc = out->message(out, "rsc-action-item", "Stop", rsc, current, NULL, stop, reason_op); STOP_SANITY_ASSERT(__LINE__); } else if (moving && current) { const bool failed = pcmk_is_set(rsc->flags, pcmk__rsc_failed); rc = out->message(out, "rsc-action-item", (failed? "Recover" : "Move"), rsc, current, next, stop, NULL); } else if (pcmk_is_set(rsc->flags, pcmk__rsc_failed)) { rc = out->message(out, "rsc-action-item", "Recover", rsc, current, NULL, stop, NULL); STOP_SANITY_ASSERT(__LINE__); } else { rc = out->message(out, "rsc-action-item", "Restart", rsc, current, next, start, NULL); #if 0 /* @TODO This can be reached in situations that should really be * "Start" (see for example the migrate-fail-7 regression test) */ STOP_SANITY_ASSERT(__LINE__); #endif } g_list_free(possible_matches); return rc; } if ((stop != NULL) && ((rsc->priv->next_role == pcmk_role_stopped) || ((start != NULL) && !pcmk_is_set(start->flags, pcmk__action_runnable)))) { key = stop_key(rsc); for (GList *iter = rsc->priv->active_nodes; iter != NULL; iter = iter->next) { pcmk_node_t *node = iter->data; pcmk_action_t *stop_op = NULL; reason_op = start; possible_matches = find_actions(rsc->priv->actions, key, node); if (possible_matches) { stop_op = possible_matches->data; g_list_free(possible_matches); } if (stop_op != NULL) { if (pcmk_is_set(stop_op->flags, pcmk__action_runnable)) { STOP_SANITY_ASSERT(__LINE__); } if (stop_op->reason != NULL) { reason_op = stop_op; } } if (out->message(out, "rsc-action-item", "Stop", rsc, node, NULL, stop_op, reason_op) == pcmk_rc_ok) { rc = pcmk_rc_ok; } } free(key); } else if ((stop != NULL) && pcmk_all_flags_set(rsc->flags, pcmk__rsc_failed |pcmk__rsc_stop_if_failed)) { /* 'stop' may be NULL if the failure was ignored */ rc = out->message(out, "rsc-action-item", "Recover", rsc, current, next, stop, start); STOP_SANITY_ASSERT(__LINE__); } else if (moving) { rc = out->message(out, "rsc-action-item", "Move", rsc, current, next, stop, NULL); STOP_SANITY_ASSERT(__LINE__); } else if (pcmk_is_set(rsc->flags, pcmk__rsc_reload)) { rc = out->message(out, "rsc-action-item", "Reload", rsc, current, next, start, NULL); } else if ((stop != NULL) && !pcmk_is_set(stop->flags, pcmk__action_optional)) { rc = out->message(out, "rsc-action-item", "Restart", rsc, current, next, start, NULL); STOP_SANITY_ASSERT(__LINE__); } else if (rsc->priv->orig_role == pcmk_role_promoted) { CRM_LOG_ASSERT(current != NULL); rc = out->message(out, "rsc-action-item", "Demote", rsc, current, next, demote, NULL); } else if (rsc->priv->next_role == pcmk_role_promoted) { CRM_LOG_ASSERT(next); rc = out->message(out, "rsc-action-item", "Promote", rsc, current, next, promote, NULL); } else if ((rsc->priv->orig_role == pcmk_role_stopped) && (rsc->priv->next_role > pcmk_role_stopped)) { rc = out->message(out, "rsc-action-item", "Start", rsc, current, next, start, NULL); } return rc; } PCMK__OUTPUT_ARGS("node-action", "const char *", "const char *", "const char *") static int node_action(pcmk__output_t *out, va_list args) { const char *task = va_arg(args, const char *); const char *node_name = va_arg(args, const char *); const char *reason = va_arg(args, const char *); if (task == NULL) { return pcmk_rc_no_output; } else if (reason) { out->list_item(out, NULL, "%s %s '%s'", task, node_name, reason); } else { crm_notice(" * %s %s", task, node_name); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("node-action", "const char *", "const char *", "const char *") static int node_action_xml(pcmk__output_t *out, va_list args) { const char *task = va_arg(args, const char *); const char *node_name = va_arg(args, const char *); const char *reason = va_arg(args, const char *); if (task == NULL) { return pcmk_rc_no_output; } else if (reason) { pcmk__output_create_xml_node(out, PCMK_XE_NODE_ACTION, PCMK_XA_TASK, task, PCMK_XA_NODE, node_name, PCMK_XA_REASON, reason, NULL); } else { crm_notice(" * %s %s", task, node_name); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("node-info", "uint32_t", "const char *", "const char *", "const char *", "bool", "bool") static int node_info_default(pcmk__output_t *out, va_list args) { uint32_t node_id = va_arg(args, uint32_t); const char *node_name = va_arg(args, const char *); const char *uuid = va_arg(args, const char *); const char *state = va_arg(args, const char *); bool have_quorum = (bool) va_arg(args, int); bool is_remote = (bool) va_arg(args, int); return out->info(out, "Node %" PRIu32 ": %s " "(uuid=%s, state=%s, have_quorum=%s, is_remote=%s)", node_id, pcmk__s(node_name, "unknown"), pcmk__s(uuid, "unknown"), pcmk__s(state, "unknown"), pcmk__btoa(have_quorum), pcmk__btoa(is_remote)); } PCMK__OUTPUT_ARGS("node-info", "uint32_t", "const char *", "const char *", "const char *", "bool", "bool") static int node_info_xml(pcmk__output_t *out, va_list args) { uint32_t node_id = va_arg(args, uint32_t); const char *node_name = va_arg(args, const char *); const char *uuid = va_arg(args, const char *); const char *state = va_arg(args, const char *); bool have_quorum = (bool) va_arg(args, int); bool is_remote = (bool) va_arg(args, int); char *id_s = crm_strdup_printf("%" PRIu32, node_id); pcmk__output_create_xml_node(out, PCMK_XE_NODE_INFO, PCMK_XA_NODEID, id_s, PCMK_XA_UNAME, node_name, PCMK_XA_ID, uuid, PCMK_XA_CRMD, state, PCMK_XA_HAVE_QUORUM, pcmk__btoa(have_quorum), PCMK_XA_REMOTE_NODE, pcmk__btoa(is_remote), NULL); free(id_s); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("inject-cluster-action", "const char *", "const char *", "xmlNode *") static int inject_cluster_action(pcmk__output_t *out, va_list args) { const char *node = va_arg(args, const char *); const char *task = va_arg(args, const char *); xmlNodePtr rsc = va_arg(args, xmlNodePtr); if (out->is_quiet(out)) { return pcmk_rc_no_output; } if (rsc != NULL) { out->list_item(out, NULL, "Cluster action: %s for %s on %s", task, pcmk__xe_id(rsc), node); } else { out->list_item(out, NULL, "Cluster action: %s on %s", task, node); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("inject-cluster-action", "const char *", "const char *", "xmlNode *") static int inject_cluster_action_xml(pcmk__output_t *out, va_list args) { const char *node = va_arg(args, const char *); const char *task = va_arg(args, const char *); xmlNodePtr rsc = va_arg(args, xmlNodePtr); xmlNodePtr xml_node = NULL; if (out->is_quiet(out)) { return pcmk_rc_no_output; } xml_node = pcmk__output_create_xml_node(out, PCMK_XE_CLUSTER_ACTION, PCMK_XA_TASK, task, PCMK_XA_NODE, node, NULL); if (rsc) { crm_xml_add(xml_node, PCMK_XA_ID, pcmk__xe_id(rsc)); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("inject-fencing-action", "const char *", "const char *") static int inject_fencing_action(pcmk__output_t *out, va_list args) { const char *target = va_arg(args, const char *); const char *op = va_arg(args, const char *); if (out->is_quiet(out)) { return pcmk_rc_no_output; } out->list_item(out, NULL, "Fencing %s (%s)", target, op); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("inject-fencing-action", "const char *", "const char *") static int inject_fencing_action_xml(pcmk__output_t *out, va_list args) { const char *target = va_arg(args, const char *); const char *op = va_arg(args, const char *); if (out->is_quiet(out)) { return pcmk_rc_no_output; } pcmk__output_create_xml_node(out, PCMK_XE_FENCING_ACTION, PCMK_XA_TARGET, target, PCMK_XA_OP, op, NULL); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("inject-attr", "const char *", "const char *", "xmlNode *") static int inject_attr(pcmk__output_t *out, va_list args) { const char *name = va_arg(args, const char *); const char *value = va_arg(args, const char *); xmlNodePtr cib_node = va_arg(args, xmlNodePtr); xmlChar *node_path = NULL; if (out->is_quiet(out)) { return pcmk_rc_no_output; } node_path = xmlGetNodePath(cib_node); out->list_item(out, NULL, "Injecting attribute %s=%s into %s '%s'", name, value, node_path, pcmk__xe_id(cib_node)); free(node_path); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("inject-attr", "const char *", "const char *", "xmlNode *") static int inject_attr_xml(pcmk__output_t *out, va_list args) { const char *name = va_arg(args, const char *); const char *value = va_arg(args, const char *); xmlNodePtr cib_node = va_arg(args, xmlNodePtr); xmlChar *node_path = NULL; if (out->is_quiet(out)) { return pcmk_rc_no_output; } node_path = xmlGetNodePath(cib_node); pcmk__output_create_xml_node(out, PCMK_XE_INJECT_ATTR, PCMK_XA_NAME, name, PCMK_XA_VALUE, value, PCMK_XA_NODE_PATH, node_path, PCMK_XA_CIB_NODE, pcmk__xe_id(cib_node), NULL); free(node_path); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("inject-spec", "const char *") static int inject_spec(pcmk__output_t *out, va_list args) { const char *spec = va_arg(args, const char *); if (out->is_quiet(out)) { return pcmk_rc_no_output; } out->list_item(out, NULL, "Injecting %s into the configuration", spec); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("inject-spec", "const char *") static int inject_spec_xml(pcmk__output_t *out, va_list args) { const char *spec = va_arg(args, const char *); if (out->is_quiet(out)) { return pcmk_rc_no_output; } pcmk__output_create_xml_node(out, PCMK_XE_INJECT_SPEC, PCMK_XA_SPEC, spec, NULL); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("inject-modify-config", "const char *", "const char *") static int inject_modify_config(pcmk__output_t *out, va_list args) { const char *quorum = va_arg(args, const char *); const char *watchdog = va_arg(args, const char *); if (out->is_quiet(out)) { return pcmk_rc_no_output; } out->begin_list(out, NULL, NULL, "Performing Requested Modifications"); if (quorum) { out->list_item(out, NULL, "Setting quorum: %s", quorum); } if (watchdog) { out->list_item(out, NULL, "Setting watchdog: %s", watchdog); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("inject-modify-config", "const char *", "const char *") static int inject_modify_config_xml(pcmk__output_t *out, va_list args) { const char *quorum = va_arg(args, const char *); const char *watchdog = va_arg(args, const char *); xmlNodePtr node = NULL; if (out->is_quiet(out)) { return pcmk_rc_no_output; } node = pcmk__output_xml_create_parent(out, PCMK_XE_MODIFICATIONS, NULL); if (quorum) { crm_xml_add(node, PCMK_XA_QUORUM, quorum); } if (watchdog) { crm_xml_add(node, PCMK_XA_WATCHDOG, watchdog); } pcmk__output_xml_pop_parent(out); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("inject-modify-node", "const char *", "const char *") static int inject_modify_node(pcmk__output_t *out, va_list args) { const char *action = va_arg(args, const char *); const char *node = va_arg(args, const char *); if (out->is_quiet(out)) { return pcmk_rc_no_output; } if (pcmk__str_eq(action, "Online", pcmk__str_none)) { out->list_item(out, NULL, "Bringing node %s online", node); return pcmk_rc_ok; } else if (pcmk__str_eq(action, "Offline", pcmk__str_none)) { out->list_item(out, NULL, "Taking node %s offline", node); return pcmk_rc_ok; } else if (pcmk__str_eq(action, "Failing", pcmk__str_none)) { out->list_item(out, NULL, "Failing node %s", node); return pcmk_rc_ok; } return pcmk_rc_no_output; } PCMK__OUTPUT_ARGS("inject-modify-node", "const char *", "const char *") static int inject_modify_node_xml(pcmk__output_t *out, va_list args) { const char *action = va_arg(args, const char *); const char *node = va_arg(args, const char *); if (out->is_quiet(out)) { return pcmk_rc_no_output; } pcmk__output_create_xml_node(out, PCMK_XE_MODIFY_NODE, PCMK_XA_ACTION, action, PCMK_XA_NODE, node, NULL); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("inject-modify-ticket", "const char *", "const char *") static int inject_modify_ticket(pcmk__output_t *out, va_list args) { const char *action = va_arg(args, const char *); const char *ticket = va_arg(args, const char *); if (out->is_quiet(out)) { return pcmk_rc_no_output; } if (pcmk__str_eq(action, "Standby", pcmk__str_none)) { out->list_item(out, NULL, "Making ticket %s standby", ticket); } else { out->list_item(out, NULL, "%s ticket %s", action, ticket); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("inject-modify-ticket", "const char *", "const char *") static int inject_modify_ticket_xml(pcmk__output_t *out, va_list args) { const char *action = va_arg(args, const char *); const char *ticket = va_arg(args, const char *); if (out->is_quiet(out)) { return pcmk_rc_no_output; } pcmk__output_create_xml_node(out, PCMK_XE_MODIFY_TICKET, PCMK_XA_ACTION, action, PCMK_XA_TICKET, ticket, NULL); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("inject-pseudo-action", "const char *", "const char *") static int inject_pseudo_action(pcmk__output_t *out, va_list args) { const char *node = va_arg(args, const char *); const char *task = va_arg(args, const char *); if (out->is_quiet(out)) { return pcmk_rc_no_output; } out->list_item(out, NULL, "Pseudo action: %s%s%s", task, ((node == NULL)? "" : " on "), pcmk__s(node, "")); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("inject-pseudo-action", "const char *", "const char *") static int inject_pseudo_action_xml(pcmk__output_t *out, va_list args) { const char *node = va_arg(args, const char *); const char *task = va_arg(args, const char *); xmlNodePtr xml_node = NULL; if (out->is_quiet(out)) { return pcmk_rc_no_output; } xml_node = pcmk__output_create_xml_node(out, PCMK_XE_PSEUDO_ACTION, PCMK_XA_TASK, task, NULL); if (node) { crm_xml_add(xml_node, PCMK_XA_NODE, node); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("inject-rsc-action", "const char *", "const char *", "const char *", "guint") static int inject_rsc_action(pcmk__output_t *out, va_list args) { const char *rsc = va_arg(args, const char *); const char *operation = va_arg(args, const char *); const char *node = va_arg(args, const char *); guint interval_ms = va_arg(args, guint); if (out->is_quiet(out)) { return pcmk_rc_no_output; } if (interval_ms) { out->list_item(out, NULL, "Resource action: %-15s %s=%u on %s", rsc, operation, interval_ms, node); } else { out->list_item(out, NULL, "Resource action: %-15s %s on %s", rsc, operation, node); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("inject-rsc-action", "const char *", "const char *", "const char *", "guint") static int inject_rsc_action_xml(pcmk__output_t *out, va_list args) { const char *rsc = va_arg(args, const char *); const char *operation = va_arg(args, const char *); const char *node = va_arg(args, const char *); guint interval_ms = va_arg(args, guint); xmlNodePtr xml_node = NULL; if (out->is_quiet(out)) { return pcmk_rc_no_output; } xml_node = pcmk__output_create_xml_node(out, PCMK_XE_RSC_ACTION, PCMK_XA_RESOURCE, rsc, PCMK_XA_OP, operation, PCMK_XA_NODE, node, NULL); if (interval_ms) { char *interval_s = pcmk__itoa(interval_ms); crm_xml_add(xml_node, PCMK_XA_INTERVAL, interval_s); free(interval_s); } return pcmk_rc_ok; } #define CHECK_RC(retcode, retval) \ if (retval == pcmk_rc_ok) { \ retcode = pcmk_rc_ok; \ } PCMK__OUTPUT_ARGS("cluster-status", "pcmk_scheduler_t *", "enum pcmk_pacemakerd_state", "crm_exit_t", "stonith_history_t *", "enum pcmk__fence_history", "uint32_t", "uint32_t", "const char *", "GList *", "GList *") int pcmk__cluster_status_text(pcmk__output_t *out, va_list args) { pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *); enum pcmk_pacemakerd_state pcmkd_state = (enum pcmk_pacemakerd_state) va_arg(args, int); crm_exit_t history_rc = va_arg(args, crm_exit_t); stonith_history_t *stonith_history = va_arg(args, stonith_history_t *); enum pcmk__fence_history fence_history = va_arg(args, int); uint32_t section_opts = va_arg(args, uint32_t); uint32_t show_opts = va_arg(args, uint32_t); const char *prefix = va_arg(args, const char *); GList *unames = va_arg(args, GList *); GList *resources = va_arg(args, GList *); int rc = pcmk_rc_no_output; bool already_printed_failure = false; CHECK_RC(rc, out->message(out, "cluster-summary", scheduler, pcmkd_state, section_opts, show_opts)); if (pcmk_is_set(section_opts, pcmk_section_nodes) && unames) { CHECK_RC(rc, out->message(out, "node-list", scheduler->nodes, unames, resources, show_opts, rc == pcmk_rc_ok)); } /* Print resources section, if needed */ if (pcmk_is_set(section_opts, pcmk_section_resources)) { CHECK_RC(rc, out->message(out, "resource-list", scheduler, show_opts, true, unames, resources, rc == pcmk_rc_ok)); } /* print Node Attributes section if requested */ if (pcmk_is_set(section_opts, pcmk_section_attributes)) { CHECK_RC(rc, out->message(out, "node-attribute-list", scheduler, show_opts, (rc == pcmk_rc_ok), unames, resources)); } /* If requested, print resource operations (which includes failcounts) * or just failcounts */ if (pcmk_any_flags_set(section_opts, pcmk_section_operations|pcmk_section_failcounts)) { CHECK_RC(rc, out->message(out, "node-summary", scheduler, unames, resources, section_opts, show_opts, (rc == pcmk_rc_ok))); } /* If there were any failed actions, print them */ if (pcmk_is_set(section_opts, pcmk_section_failures) && (scheduler->priv->failed != NULL) && (scheduler->priv->failed->children != NULL)) { CHECK_RC(rc, out->message(out, "failed-action-list", scheduler, unames, resources, show_opts, rc == pcmk_rc_ok)); } /* Print failed stonith actions */ if (pcmk_is_set(section_opts, pcmk_section_fence_failed) && fence_history != pcmk__fence_history_none) { if (history_rc == 0) { stonith_history_t *hp = NULL; hp = stonith__first_matching_event(stonith_history, stonith__event_state_eq, GINT_TO_POINTER(st_failed)); if (hp) { CHECK_RC(rc, out->message(out, "failed-fencing-list", stonith_history, unames, section_opts, show_opts, rc == pcmk_rc_ok)); } } else { PCMK__OUTPUT_SPACER_IF(out, rc == pcmk_rc_ok); out->begin_list(out, NULL, NULL, "Failed Fencing Actions"); out->list_item(out, NULL, "Failed to get fencing history: %s", crm_exit_str(history_rc)); out->end_list(out); already_printed_failure = true; } } /* Print tickets if requested */ if (pcmk_is_set(section_opts, pcmk_section_tickets)) { CHECK_RC(rc, out->message(out, "ticket-list", scheduler->priv->ticket_constraints, (rc == pcmk_rc_ok), false, false)); } /* Print negative location constraints if requested */ if (pcmk_is_set(section_opts, pcmk_section_bans)) { CHECK_RC(rc, out->message(out, "ban-list", scheduler, prefix, resources, show_opts, rc == pcmk_rc_ok)); } /* Print stonith history */ if (pcmk_any_flags_set(section_opts, pcmk_section_fencing_all) && fence_history != pcmk__fence_history_none) { if (history_rc != 0) { if (!already_printed_failure) { PCMK__OUTPUT_SPACER_IF(out, rc == pcmk_rc_ok); out->begin_list(out, NULL, NULL, "Failed Fencing Actions"); out->list_item(out, NULL, "Failed to get fencing history: %s", crm_exit_str(history_rc)); out->end_list(out); } } else if (pcmk_is_set(section_opts, pcmk_section_fence_worked)) { stonith_history_t *hp = NULL; hp = stonith__first_matching_event(stonith_history, stonith__event_state_neq, GINT_TO_POINTER(st_failed)); if (hp) { CHECK_RC(rc, out->message(out, "fencing-list", hp, unames, section_opts, show_opts, rc == pcmk_rc_ok)); } } else if (pcmk_is_set(section_opts, pcmk_section_fence_pending)) { stonith_history_t *hp = NULL; hp = stonith__first_matching_event(stonith_history, stonith__event_state_pending, NULL); if (hp) { CHECK_RC(rc, out->message(out, "pending-fencing-list", hp, unames, section_opts, show_opts, rc == pcmk_rc_ok)); } } } return rc; } PCMK__OUTPUT_ARGS("cluster-status", "pcmk_scheduler_t *", "enum pcmk_pacemakerd_state", "crm_exit_t", "stonith_history_t *", "enum pcmk__fence_history", "uint32_t", "uint32_t", "const char *", "GList *", "GList *") static int cluster_status_xml(pcmk__output_t *out, va_list args) { pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *); enum pcmk_pacemakerd_state pcmkd_state = (enum pcmk_pacemakerd_state) va_arg(args, int); crm_exit_t history_rc = va_arg(args, crm_exit_t); stonith_history_t *stonith_history = va_arg(args, stonith_history_t *); enum pcmk__fence_history fence_history = va_arg(args, int); uint32_t section_opts = va_arg(args, uint32_t); uint32_t show_opts = va_arg(args, uint32_t); const char *prefix = va_arg(args, const char *); GList *unames = va_arg(args, GList *); GList *resources = va_arg(args, GList *); out->message(out, "cluster-summary", scheduler, pcmkd_state, section_opts, show_opts); /*** NODES ***/ if (pcmk_is_set(section_opts, pcmk_section_nodes)) { out->message(out, "node-list", scheduler->nodes, unames, resources, show_opts, false); } /* Print resources section, if needed */ if (pcmk_is_set(section_opts, pcmk_section_resources)) { /* XML output always displays full details. */ uint32_t full_show_opts = show_opts & ~pcmk_show_brief; out->message(out, "resource-list", scheduler, full_show_opts, false, unames, resources, false); } /* print Node Attributes section if requested */ if (pcmk_is_set(section_opts, pcmk_section_attributes)) { out->message(out, "node-attribute-list", scheduler, show_opts, false, unames, resources); } /* If requested, print resource operations (which includes failcounts) * or just failcounts */ if (pcmk_any_flags_set(section_opts, pcmk_section_operations|pcmk_section_failcounts)) { out->message(out, "node-summary", scheduler, unames, resources, section_opts, show_opts, false); } /* If there were any failed actions, print them */ if (pcmk_is_set(section_opts, pcmk_section_failures) && (scheduler->priv->failed != NULL) && (scheduler->priv->failed->children != NULL)) { out->message(out, "failed-action-list", scheduler, unames, resources, show_opts, false); } /* Print stonith history */ if (pcmk_is_set(section_opts, pcmk_section_fencing_all) && fence_history != pcmk__fence_history_none) { out->message(out, "full-fencing-list", history_rc, stonith_history, unames, section_opts, show_opts, false); } /* Print tickets if requested */ if (pcmk_is_set(section_opts, pcmk_section_tickets)) { out->message(out, "ticket-list", scheduler->priv->ticket_constraints, false, false, false); } /* Print negative location constraints if requested */ if (pcmk_is_set(section_opts, pcmk_section_bans)) { out->message(out, "ban-list", scheduler, prefix, resources, show_opts, false); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("cluster-status", "pcmk_scheduler_t *", "enum pcmk_pacemakerd_state", "crm_exit_t", "stonith_history_t *", "enum pcmk__fence_history", "uint32_t", "uint32_t", "const char *", "GList *", "GList *") static int cluster_status_html(pcmk__output_t *out, va_list args) { pcmk_scheduler_t *scheduler = va_arg(args, pcmk_scheduler_t *); enum pcmk_pacemakerd_state pcmkd_state = (enum pcmk_pacemakerd_state) va_arg(args, int); crm_exit_t history_rc = va_arg(args, crm_exit_t); stonith_history_t *stonith_history = va_arg(args, stonith_history_t *); enum pcmk__fence_history fence_history = va_arg(args, int); uint32_t section_opts = va_arg(args, uint32_t); uint32_t show_opts = va_arg(args, uint32_t); const char *prefix = va_arg(args, const char *); GList *unames = va_arg(args, GList *); GList *resources = va_arg(args, GList *); bool already_printed_failure = false; out->message(out, "cluster-summary", scheduler, pcmkd_state, section_opts, show_opts); /*** NODE LIST ***/ if (pcmk_is_set(section_opts, pcmk_section_nodes) && unames) { out->message(out, "node-list", scheduler->nodes, unames, resources, show_opts, false); } /* Print resources section, if needed */ if (pcmk_is_set(section_opts, pcmk_section_resources)) { out->message(out, "resource-list", scheduler, show_opts, true, unames, resources, false); } /* print Node Attributes section if requested */ if (pcmk_is_set(section_opts, pcmk_section_attributes)) { out->message(out, "node-attribute-list", scheduler, show_opts, false, unames, resources); } /* If requested, print resource operations (which includes failcounts) * or just failcounts */ if (pcmk_any_flags_set(section_opts, pcmk_section_operations|pcmk_section_failcounts)) { out->message(out, "node-summary", scheduler, unames, resources, section_opts, show_opts, false); } /* If there were any failed actions, print them */ if (pcmk_is_set(section_opts, pcmk_section_failures) && (scheduler->priv->failed != NULL) && (scheduler->priv->failed->children != NULL)) { out->message(out, "failed-action-list", scheduler, unames, resources, show_opts, false); } /* Print failed stonith actions */ if (pcmk_is_set(section_opts, pcmk_section_fence_failed) && fence_history != pcmk__fence_history_none) { if (history_rc == 0) { stonith_history_t *hp = NULL; hp = stonith__first_matching_event(stonith_history, stonith__event_state_eq, GINT_TO_POINTER(st_failed)); if (hp) { out->message(out, "failed-fencing-list", stonith_history, unames, section_opts, show_opts, false); } } else { out->begin_list(out, NULL, NULL, "Failed Fencing Actions"); out->list_item(out, NULL, "Failed to get fencing history: %s", crm_exit_str(history_rc)); out->end_list(out); } } /* Print stonith history */ if (pcmk_any_flags_set(section_opts, pcmk_section_fencing_all) && fence_history != pcmk__fence_history_none) { if (history_rc != 0) { if (!already_printed_failure) { out->begin_list(out, NULL, NULL, "Failed Fencing Actions"); out->list_item(out, NULL, "Failed to get fencing history: %s", crm_exit_str(history_rc)); out->end_list(out); } } else if (pcmk_is_set(section_opts, pcmk_section_fence_worked)) { stonith_history_t *hp = NULL; hp = stonith__first_matching_event(stonith_history, stonith__event_state_neq, GINT_TO_POINTER(st_failed)); if (hp) { out->message(out, "fencing-list", hp, unames, section_opts, show_opts, false); } } else if (pcmk_is_set(section_opts, pcmk_section_fence_pending)) { stonith_history_t *hp = NULL; hp = stonith__first_matching_event(stonith_history, stonith__event_state_pending, NULL); if (hp) { out->message(out, "pending-fencing-list", hp, unames, section_opts, show_opts, false); } } } /* Print tickets if requested */ if (pcmk_is_set(section_opts, pcmk_section_tickets)) { out->message(out, "ticket-list", scheduler->priv->ticket_constraints, false, false, false); } /* Print negative location constraints if requested */ if (pcmk_is_set(section_opts, pcmk_section_bans)) { out->message(out, "ban-list", scheduler, prefix, resources, show_opts, false); } return pcmk_rc_ok; } #define KV_PAIR(k, v) do { \ if (legacy) { \ pcmk__g_strcat(s, k "=", pcmk__s(v, ""), " ", NULL); \ } else { \ pcmk__g_strcat(s, k "=\"", pcmk__s(v, ""), "\" ", NULL); \ } \ } while (0) PCMK__OUTPUT_ARGS("attribute", "const char *", "const char *", "const char *", "const char *", "const char *", "bool", "bool") static int attribute_default(pcmk__output_t *out, va_list args) { const char *scope = va_arg(args, const char *); const char *instance = va_arg(args, const char *); const char *name = va_arg(args, const char *); const char *value = va_arg(args, const char *); const char *host = va_arg(args, const char *); bool quiet = va_arg(args, int); bool legacy = va_arg(args, int); gchar *value_esc = NULL; GString *s = NULL; if (quiet) { if (value != NULL) { /* Quiet needs to be turned off for ->info() to do anything */ bool was_quiet = out->is_quiet(out); if (was_quiet) { out->quiet = false; } out->info(out, "%s", value); out->quiet = was_quiet; } return pcmk_rc_ok; } s = g_string_sized_new(50); if (pcmk__xml_needs_escape(value, pcmk__xml_escape_attr_pretty)) { value_esc = pcmk__xml_escape(value, pcmk__xml_escape_attr_pretty); value = value_esc; } if (!pcmk__str_empty(scope)) { KV_PAIR(PCMK_XA_SCOPE, scope); } if (!pcmk__str_empty(instance)) { KV_PAIR(PCMK_XA_ID, instance); } KV_PAIR(PCMK_XA_NAME, name); if (!pcmk__str_empty(host)) { KV_PAIR(PCMK_XA_HOST, host); } if (legacy) { pcmk__g_strcat(s, PCMK_XA_VALUE "=", pcmk__s(value, "(null)"), NULL); } else { pcmk__g_strcat(s, PCMK_XA_VALUE "=\"", pcmk__s(value, ""), "\"", NULL); } out->info(out, "%s", s->str); g_free(value_esc); g_string_free(s, TRUE); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("attribute", "const char *", "const char *", "const char *", "const char *", "const char *", "bool", "bool") static int attribute_xml(pcmk__output_t *out, va_list args) { const char *scope = va_arg(args, const char *); const char *instance = va_arg(args, const char *); const char *name = va_arg(args, const char *); const char *value = va_arg(args, const char *); const char *host = va_arg(args, const char *); bool quiet G_GNUC_UNUSED = va_arg(args, int); bool legacy G_GNUC_UNUSED = va_arg(args, int); xmlNodePtr node = NULL; node = pcmk__output_create_xml_node(out, PCMK_XE_ATTRIBUTE, PCMK_XA_NAME, name, PCMK_XA_VALUE, pcmk__s(value, ""), NULL); if (!pcmk__str_empty(scope)) { crm_xml_add(node, PCMK_XA_SCOPE, scope); } if (!pcmk__str_empty(instance)) { crm_xml_add(node, PCMK_XA_ID, instance); } if (!pcmk__str_empty(host)) { crm_xml_add(node, PCMK_XA_HOST, host); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("rule-check", "const char *", "int", "const char *") static int rule_check_default(pcmk__output_t *out, va_list args) { const char *rule_id = va_arg(args, const char *); int result = va_arg(args, int); const char *error = va_arg(args, const char *); switch (result) { case pcmk_rc_within_range: return out->info(out, "Rule %s is still in effect", rule_id); case pcmk_rc_ok: return out->info(out, "Rule %s satisfies conditions", rule_id); case pcmk_rc_after_range: return out->info(out, "Rule %s is expired", rule_id); case pcmk_rc_before_range: return out->info(out, "Rule %s has not yet taken effect", rule_id); case pcmk_rc_op_unsatisfied: return out->info(out, "Rule %s does not satisfy conditions", rule_id); default: out->err(out, "Could not determine whether rule %s is in effect: %s", rule_id, ((error != NULL)? error : "unexpected error")); return pcmk_rc_ok; } } PCMK__OUTPUT_ARGS("rule-check", "const char *", "int", "const char *") static int rule_check_xml(pcmk__output_t *out, va_list args) { const char *rule_id = va_arg(args, const char *); int result = va_arg(args, int); const char *error = va_arg(args, const char *); char *rc_str = pcmk__itoa(pcmk_rc2exitc(result)); pcmk__output_create_xml_node(out, PCMK_XE_RULE_CHECK, PCMK_XA_RULE_ID, rule_id, PCMK_XA_RC, rc_str, NULL); free(rc_str); switch (result) { case pcmk_rc_within_range: case pcmk_rc_ok: case pcmk_rc_after_range: case pcmk_rc_before_range: case pcmk_rc_op_unsatisfied: return pcmk_rc_ok; default: out->err(out, "Could not determine whether rule %s is in effect: %s", rule_id, ((error != NULL)? error : "unexpected error")); return pcmk_rc_ok; } } PCMK__OUTPUT_ARGS("result-code", "int", "const char *", "const char *") static int result_code_none(pcmk__output_t *out, va_list args) { return pcmk_rc_no_output; } PCMK__OUTPUT_ARGS("result-code", "int", "const char *", "const char *") static int result_code_text(pcmk__output_t *out, va_list args) { int code = va_arg(args, int); const char *name = va_arg(args, const char *); const char *desc = va_arg(args, const char *); static int code_width = 0; if (out->is_quiet(out)) { /* If out->is_quiet(), don't print the code. Print name and/or desc in a * compact format for text output, or print nothing at all for none-type * output. */ if ((name != NULL) && (desc != NULL)) { pcmk__formatted_printf(out, "%s - %s\n", name, desc); } else if ((name != NULL) || (desc != NULL)) { pcmk__formatted_printf(out, "%s\n", ((name != NULL)? name : desc)); } return pcmk_rc_ok; } /* Get length of longest (most negative) standard Pacemaker return code * This should be longer than all the values of any other type of return * code. */ if (code_width == 0) { long long most_negative = pcmk_rc_error - (long long) pcmk__n_rc + 1; code_width = (int) snprintf(NULL, 0, "%lld", most_negative); } if ((name != NULL) && (desc != NULL)) { static int name_width = 0; if (name_width == 0) { // Get length of longest standard Pacemaker return code name for (int lpc = 0; lpc < pcmk__n_rc; lpc++) { int len = (int) strlen(pcmk_rc_name(pcmk_rc_error - lpc)); name_width = QB_MAX(name_width, len); } } return out->info(out, "% *d: %-*s %s", code_width, code, name_width, name, desc); } if ((name != NULL) || (desc != NULL)) { return out->info(out, "% *d: %s", code_width, code, ((name != NULL)? name : desc)); } return out->info(out, "% *d", code_width, code); } PCMK__OUTPUT_ARGS("result-code", "int", "const char *", "const char *") static int result_code_xml(pcmk__output_t *out, va_list args) { int code = va_arg(args, int); const char *name = va_arg(args, const char *); const char *desc = va_arg(args, const char *); char *code_str = pcmk__itoa(code); pcmk__output_create_xml_node(out, PCMK_XE_RESULT_CODE, PCMK_XA_CODE, code_str, PCMK_XA_NAME, name, PCMK_XA_DESCRIPTION, desc, NULL); free(code_str); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("ticket-attribute", "const char *", "const char *", "const char *") static int ticket_attribute_default(pcmk__output_t *out, va_list args) { const char *ticket_id G_GNUC_UNUSED = va_arg(args, const char *); const char *name G_GNUC_UNUSED = va_arg(args, const char *); const char *value = va_arg(args, const char *); out->info(out, "%s", value); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("ticket-attribute", "const char *", "const char *", "const char *") static int ticket_attribute_xml(pcmk__output_t *out, va_list args) { const char *ticket_id = va_arg(args, const char *); const char *name = va_arg(args, const char *); const char *value = va_arg(args, const char *); /* Create: * * * * * */ pcmk__output_xml_create_parent(out, PCMK_XE_TICKETS, NULL); pcmk__output_xml_create_parent(out, PCMK_XE_TICKET, PCMK_XA_ID, ticket_id, NULL); pcmk__output_create_xml_node(out, PCMK_XA_ATTRIBUTE, PCMK_XA_NAME, name, PCMK_XA_VALUE, value, NULL); pcmk__output_xml_pop_parent(out); pcmk__output_xml_pop_parent(out); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("ticket-constraints", "xmlNode *") static int ticket_constraints_default(pcmk__output_t *out, va_list args) { xmlNode *constraint_xml = va_arg(args, xmlNode *); /* constraint_xml can take two forms: * * * * for when there's only one ticket in the CIB, or when the user asked * for a specific ticket (crm_ticket -c -t for instance) * * * * * * * for when there's multiple tickets in the and the user did not ask for * a specific one. * * In both cases, we simply output a element for each ticket * in the results. */ out->info(out, "Constraints XML:\n"); if (pcmk__xe_is(constraint_xml, PCMK__XE_XPATH_QUERY)) { xmlNode *child = pcmk__xe_first_child(constraint_xml, NULL, NULL, NULL); do { GString *buf = g_string_sized_new(1024); pcmk__xml_string(child, pcmk__xml_fmt_pretty, buf, 0); out->output_xml(out, PCMK_XE_CONSTRAINT, buf->str); g_string_free(buf, TRUE); child = pcmk__xe_next(child); } while (child != NULL); } else { GString *buf = g_string_sized_new(1024); pcmk__xml_string(constraint_xml, pcmk__xml_fmt_pretty, buf, 0); out->output_xml(out, PCMK_XE_CONSTRAINT, buf->str); g_string_free(buf, TRUE); } return pcmk_rc_ok; } static int add_ticket_element_with_constraints(xmlNode *node, void *userdata) { pcmk__output_t *out = (pcmk__output_t *) userdata; const char *ticket_id = crm_element_value(node, PCMK_XA_TICKET); pcmk__output_xml_create_parent(out, PCMK_XE_TICKET, PCMK_XA_ID, ticket_id, NULL); pcmk__output_xml_create_parent(out, PCMK_XE_CONSTRAINTS, NULL); pcmk__output_xml_add_node_copy(out, node); /* Pop two parents so now we are back under the element */ pcmk__output_xml_pop_parent(out); pcmk__output_xml_pop_parent(out); return pcmk_rc_ok; } static int add_resource_element(xmlNode *node, void *userdata) { pcmk__output_t *out = (pcmk__output_t *) userdata; const char *rsc = crm_element_value(node, PCMK_XA_RSC); pcmk__output_create_xml_node(out, PCMK_XE_RESOURCE, PCMK_XA_ID, rsc, NULL); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("ticket-constraints", "xmlNode *") static int ticket_constraints_xml(pcmk__output_t *out, va_list args) { xmlNode *constraint_xml = va_arg(args, xmlNode *); /* Create: * * * * * * * ... * */ pcmk__output_xml_create_parent(out, PCMK_XE_TICKETS, NULL); if (pcmk__xe_is(constraint_xml, PCMK__XE_XPATH_QUERY)) { /* Iterate through the list of children once to create all the * ticket/constraint elements. */ pcmk__xe_foreach_child(constraint_xml, NULL, add_ticket_element_with_constraints, out); /* Put us back at the same level as where was created. */ pcmk__output_xml_pop_parent(out); /* Constraints can reference a resource ID that is defined in the XML * schema as an IDREF. This requires some other element to be present * with an id= attribute that matches. * * Iterate through the list of children a second time to create the * following: * * * * ... * */ pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCES, NULL); pcmk__xe_foreach_child(constraint_xml, NULL, add_resource_element, out); pcmk__output_xml_pop_parent(out); } else { /* Creating the output for a single constraint is much easier. All the * comments in the above block apply here. */ add_ticket_element_with_constraints(constraint_xml, out); pcmk__output_xml_pop_parent(out); pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCES, NULL); add_resource_element(constraint_xml, out); pcmk__output_xml_pop_parent(out); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("ticket-state", "xmlNode *") static int ticket_state_default(pcmk__output_t *out, va_list args) { xmlNode *state_xml = va_arg(args, xmlNode *); GString *buf = g_string_sized_new(1024); out->info(out, "State XML:\n"); pcmk__xml_string(state_xml, pcmk__xml_fmt_pretty, buf, 0); out->output_xml(out, PCMK__XE_TICKET_STATE, buf->str); g_string_free(buf, TRUE); return pcmk_rc_ok; } static int add_ticket_element(xmlNode *node, void *userdata) { pcmk__output_t *out = (pcmk__output_t *) userdata; xmlNode *ticket_node = NULL; ticket_node = pcmk__output_create_xml_node(out, PCMK_XE_TICKET, NULL); pcmk__xe_copy_attrs(ticket_node, node, pcmk__xaf_none); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("ticket-state", "xmlNode *") static int ticket_state_xml(pcmk__output_t *out, va_list args) { xmlNode *state_xml = va_arg(args, xmlNode *); /* Create: * * * ... * */ pcmk__output_xml_create_parent(out, PCMK_XE_TICKETS, NULL); if (state_xml->children != NULL) { /* Iterate through the list of children once to create all the * ticket elements. */ pcmk__xe_foreach_child(state_xml, PCMK__XE_TICKET_STATE, add_ticket_element, out); } else { add_ticket_element(state_xml, out); } pcmk__output_xml_pop_parent(out); return pcmk_rc_ok; } static pcmk__message_entry_t fmt_functions[] = { { "attribute", "default", attribute_default }, { "attribute", "xml", attribute_xml }, { "cluster-status", "default", pcmk__cluster_status_text }, { "cluster-status", "html", cluster_status_html }, { "cluster-status", "xml", cluster_status_xml }, { "crmadmin-node", "default", crmadmin_node }, { "crmadmin-node", "text", crmadmin_node_text }, { "crmadmin-node", "xml", crmadmin_node_xml }, { "dc", "default", dc }, { "dc", "text", dc_text }, { "dc", "xml", dc_xml }, { "digests", "default", digests_text }, { "digests", "xml", digests_xml }, { "health", "default", health }, { "health", "text", health_text }, { "health", "xml", health_xml }, { "inject-attr", "default", inject_attr }, { "inject-attr", "xml", inject_attr_xml }, { "inject-cluster-action", "default", inject_cluster_action }, { "inject-cluster-action", "xml", inject_cluster_action_xml }, { "inject-fencing-action", "default", inject_fencing_action }, { "inject-fencing-action", "xml", inject_fencing_action_xml }, { "inject-modify-config", "default", inject_modify_config }, { "inject-modify-config", "xml", inject_modify_config_xml }, { "inject-modify-node", "default", inject_modify_node }, { "inject-modify-node", "xml", inject_modify_node_xml }, { "inject-modify-ticket", "default", inject_modify_ticket }, { "inject-modify-ticket", "xml", inject_modify_ticket_xml }, { "inject-pseudo-action", "default", inject_pseudo_action }, { "inject-pseudo-action", "xml", inject_pseudo_action_xml }, { "inject-rsc-action", "default", inject_rsc_action }, { "inject-rsc-action", "xml", inject_rsc_action_xml }, { "inject-spec", "default", inject_spec }, { "inject-spec", "xml", inject_spec_xml }, { "locations-and-colocations", "default", locations_and_colocations }, { "locations-and-colocations", "xml", locations_and_colocations_xml }, { "locations-list", "default", locations_list }, { "locations-list", "xml", locations_list_xml }, { "node-action", "default", node_action }, { "node-action", "xml", node_action_xml }, { "node-info", "default", node_info_default }, { "node-info", "xml", node_info_xml }, { "pacemakerd-health", "default", pacemakerd_health }, { "pacemakerd-health", "html", pacemakerd_health_html }, { "pacemakerd-health", "text", pacemakerd_health_text }, { "pacemakerd-health", "xml", pacemakerd_health_xml }, { "profile", "default", profile_default, }, { "profile", "xml", profile_xml }, { "result-code", PCMK_VALUE_NONE, result_code_none }, { "result-code", "text", result_code_text }, { "result-code", "xml", result_code_xml }, { "rsc-action", "default", rsc_action_default }, { "rsc-action-item", "default", rsc_action_item }, { "rsc-action-item", "xml", rsc_action_item_xml }, { "rsc-is-colocated-with-list", "default", rsc_is_colocated_with_list }, { "rsc-is-colocated-with-list", "xml", rsc_is_colocated_with_list_xml }, { "rscs-colocated-with-list", "default", rscs_colocated_with_list }, { "rscs-colocated-with-list", "xml", rscs_colocated_with_list_xml }, { "rule-check", "default", rule_check_default }, { "rule-check", "xml", rule_check_xml }, { "ticket-attribute", "default", ticket_attribute_default }, { "ticket-attribute", "xml", ticket_attribute_xml }, { "ticket-constraints", "default", ticket_constraints_default }, { "ticket-constraints", "xml", ticket_constraints_xml }, { "ticket-state", "default", ticket_state_default }, { "ticket-state", "xml", ticket_state_xml }, { NULL, NULL, NULL } }; void pcmk__register_lib_messages(pcmk__output_t *out) { pcmk__register_messages(out, fmt_functions); } diff --git a/lib/pacemaker/pcmk_sched_constraints.c b/lib/pacemaker/pcmk_sched_constraints.c index 8cefce8752..06e89c5887 100644 --- a/lib/pacemaker/pcmk_sched_constraints.c +++ b/lib/pacemaker/pcmk_sched_constraints.c @@ -1,477 +1,474 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "libpacemaker_private.h" static bool evaluate_lifetime(xmlNode *lifetime, pcmk_scheduler_t *scheduler) { bool result = false; crm_time_t *next_change = crm_time_new_undefined(); pcmk_rule_input_t rule_input = { .now = scheduler->priv->now, }; result = (pcmk__evaluate_rules(lifetime, &rule_input, next_change) == pcmk_rc_ok); if (crm_time_is_defined(next_change)) { time_t recheck = (time_t) crm_time_get_seconds_since_epoch(next_change); pe__update_recheck_time(recheck, scheduler, "constraint lifetime"); } crm_time_free(next_change); return result; } /*! * \internal * \brief Unpack constraints from XML * * Given scheduler data, unpack all constraints from its input XML into * data structures. * * \param[in,out] scheduler Scheduler data */ void pcmk__unpack_constraints(pcmk_scheduler_t *scheduler) { xmlNode *xml_constraints = pcmk_find_cib_element(scheduler->input, PCMK_XE_CONSTRAINTS); for (xmlNode *xml_obj = pcmk__xe_first_child(xml_constraints, NULL, NULL, NULL); xml_obj != NULL; xml_obj = pcmk__xe_next(xml_obj)) { xmlNode *lifetime = NULL; const char *id = crm_element_value(xml_obj, PCMK_XA_ID); const char *tag = (const char *) xml_obj->name; if (id == NULL) { pcmk__config_err("Ignoring <%s> constraint without " PCMK_XA_ID, tag); continue; } crm_trace("Unpacking %s constraint '%s'", tag, id); lifetime = pcmk__xe_first_child(xml_obj, PCMK__XE_LIFETIME, NULL, NULL); if (lifetime != NULL) { pcmk__config_warn("Support for '" PCMK__XE_LIFETIME "' element " "(in %s) is deprecated and will be dropped " "in a later release", id); } if ((lifetime != NULL) && !evaluate_lifetime(lifetime, scheduler)) { crm_info("Constraint %s %s is not active", tag, id); } else if (pcmk__str_eq(PCMK_XE_RSC_ORDER, tag, pcmk__str_none)) { pcmk__unpack_ordering(xml_obj, scheduler); } else if (pcmk__str_eq(PCMK_XE_RSC_COLOCATION, tag, pcmk__str_none)) { pcmk__unpack_colocation(xml_obj, scheduler); } else if (pcmk__str_eq(PCMK_XE_RSC_LOCATION, tag, pcmk__str_none)) { pcmk__unpack_location(xml_obj, scheduler); } else if (pcmk__str_eq(PCMK_XE_RSC_TICKET, tag, pcmk__str_none)) { pcmk__unpack_rsc_ticket(xml_obj, scheduler); } else { pcmk__config_err("Unsupported constraint type: %s", tag); } } } pcmk_resource_t * pcmk__find_constraint_resource(GList *rsc_list, const char *id) { if (id == NULL) { return NULL; } for (GList *iter = rsc_list; iter != NULL; iter = iter->next) { pcmk_resource_t *parent = iter->data; pcmk_resource_t *match = NULL; match = parent->priv->fns->find_rsc(parent, id, NULL, pcmk_rsc_match_history); if (match != NULL) { if (!pcmk__str_eq(match->id, id, pcmk__str_none)) { /* We found an instance of a clone instead */ match = uber_parent(match); crm_debug("Found %s for %s", match->id, id); } return match; } } crm_trace("No match for %s", id); return NULL; } /*! * \internal * \brief Check whether an ID references a resource tag * * \param[in] scheduler Scheduler data * \param[in] id Tag ID to search for * \param[out] tag Where to store tag, if found * * \return true if ID refers to a tagged resource or resource set template, * otherwise false */ static bool find_constraint_tag(const pcmk_scheduler_t *scheduler, const char *id, pcmk__idref_t **tag) { *tag = NULL; // Check whether id refers to a resource set template if (g_hash_table_lookup_extended(scheduler->priv->templates, id, NULL, (gpointer *) tag)) { if (*tag == NULL) { crm_notice("No resource is derived from template '%s'", id); return false; } return true; } // If not, check whether id refers to a tag if (g_hash_table_lookup_extended(scheduler->priv->tags, id, NULL, (gpointer *) tag)) { if (*tag == NULL) { crm_notice("No resource is tagged with '%s'", id); return false; } return true; } pcmk__config_warn("No resource, template, or tag named '%s'", id); return false; } /*! * \internal * \brief Parse a role attribute from a constraint * * This is like pcmk_parse_role() except that started is treated as * pcmk_role_unknown (indicating any role), and the return value is * pcmk_rc_unpack_error for invalid specifications. * * \param[in] id ID of constraint being parsed (for logging only) * \param[in] role_spec Role specification * \param[in] role Where to store parsed role * * \return Standard Pacemaker return code */ int pcmk__parse_constraint_role(const char *id, const char *role_spec, enum rsc_role_e *role) { *role = pcmk_parse_role(role_spec); switch (*role) { case pcmk_role_unknown: if (role_spec != NULL) { pcmk__config_err("Ignoring constraint %s: Invalid role '%s'", id, role_spec); return pcmk_rc_unpack_error; } break; case pcmk_role_started: *role = pcmk_role_unknown; break; default: break; } return pcmk_rc_ok; } /*! * \brief * \internal Check whether an ID refers to a valid resource or tag * * \param[in] scheduler Scheduler data * \param[in] id ID to search for * \param[out] rsc Where to store resource, if found * (or NULL to skip searching resources) * \param[out] tag Where to store tag, if found * (or NULL to skip searching tags) * * \return true if id refers to a resource (possibly indirectly via a tag) */ bool pcmk__valid_resource_or_tag(const pcmk_scheduler_t *scheduler, const char *id, pcmk_resource_t **rsc, pcmk__idref_t **tag) { if (rsc != NULL) { *rsc = pcmk__find_constraint_resource(scheduler->priv->resources, id); if (*rsc != NULL) { return true; } } if ((tag != NULL) && find_constraint_tag(scheduler, id, tag)) { return true; } return false; } /*! * \internal * \brief Replace any resource tags with equivalent \C PCMK_XE_RESOURCE_REF * entries * * If a given constraint has resource sets, check each set for * \c PCMK_XE_RESOURCE_REF entries that list tags rather than resource IDs, and * replace any found with \c PCMK_XE_RESOURCE_REF entries for the corresponding * resource IDs. * * \param[in,out] xml_obj Constraint XML * \param[in] scheduler Scheduler data * * \return Equivalent XML with resource tags replaced (or NULL if none) * \note It is the caller's responsibility to free the return value with * \c pcmk__xml_free(). */ xmlNode * pcmk__expand_tags_in_sets(xmlNode *xml_obj, const pcmk_scheduler_t *scheduler) { xmlNode *new_xml = NULL; bool any_refs = false; // Short-circuit if there are no sets if (pcmk__xe_first_child(xml_obj, PCMK_XE_RESOURCE_SET, NULL, NULL) == NULL) { return NULL; } new_xml = pcmk__xml_copy(NULL, xml_obj); for (xmlNode *set = pcmk__xe_first_child(new_xml, PCMK_XE_RESOURCE_SET, NULL, NULL); set != NULL; set = pcmk__xe_next_same(set)) { GList *tag_refs = NULL; GList *iter = NULL; for (xmlNode *xml_rsc = pcmk__xe_first_child(set, PCMK_XE_RESOURCE_REF, NULL, NULL); xml_rsc != NULL; xml_rsc = pcmk__xe_next_same(xml_rsc)) { pcmk_resource_t *rsc = NULL; pcmk__idref_t *tag = NULL; if (!pcmk__valid_resource_or_tag(scheduler, pcmk__xe_id(xml_rsc), &rsc, &tag)) { pcmk__config_err("Ignoring resource sets for constraint '%s' " "because '%s' is not a valid resource or tag", pcmk__xe_id(xml_obj), pcmk__xe_id(xml_rsc)); pcmk__xml_free(new_xml); return NULL; } else if (rsc) { continue; } else if (tag) { /* PCMK_XE_RESOURCE_REF under PCMK_XE_RESOURCE_SET references * template or tag */ xmlNode *last_ref = xml_rsc; /* For example, given the original XML: * * * * * * * * If rsc2 and rsc3 are tagged with tag1, we add them after it: * * * * * * * * */ for (iter = tag->refs; iter != NULL; iter = iter->next) { - const char *obj_ref = iter->data; - xmlNode *new_rsc_ref = NULL; + const char *ref_id = iter->data; + xmlNode *new_ref = pcmk__xe_create(set, + PCMK_XE_RESOURCE_REF); - new_rsc_ref = xmlNewDocRawNode(set->doc, NULL, - (pcmkXmlStr) - PCMK_XE_RESOURCE_REF, - NULL); - crm_xml_add(new_rsc_ref, PCMK_XA_ID, obj_ref); - xmlAddNextSibling(last_ref, new_rsc_ref); + crm_xml_add(new_ref, PCMK_XA_ID, ref_id); + xmlAddNextSibling(last_ref, new_ref); - last_ref = new_rsc_ref; + last_ref = new_ref; } any_refs = true; /* Freeing the resource_ref now would break the XML child * iteration, so just remember it for freeing later. */ tag_refs = g_list_append(tag_refs, xml_rsc); } } /* Now free '', and finally get: */ for (iter = tag_refs; iter != NULL; iter = iter->next) { xmlNode *tag_ref = iter->data; pcmk__xml_free(tag_ref); } g_list_free(tag_refs); } if (!any_refs) { pcmk__xml_free(new_xml); new_xml = NULL; } return new_xml; } /*! * \internal * \brief Convert a tag into a resource set of tagged resources * * \param[in,out] xml_obj Constraint XML * \param[out] rsc_set Where to store resource set XML * \param[in] attr Name of XML attribute with resource or tag ID * \param[in] convert_rsc If true, convert to set even if \p attr * references a resource * \param[in] scheduler Scheduler data */ bool pcmk__tag_to_set(xmlNode *xml_obj, xmlNode **rsc_set, const char *attr, bool convert_rsc, const pcmk_scheduler_t *scheduler) { const char *cons_id = NULL; const char *id = NULL; pcmk_resource_t *rsc = NULL; pcmk__idref_t *tag = NULL; *rsc_set = NULL; CRM_CHECK((xml_obj != NULL) && (attr != NULL), return false); cons_id = pcmk__xe_id(xml_obj); if (cons_id == NULL) { pcmk__config_err("Ignoring <%s> constraint without " PCMK_XA_ID, xml_obj->name); return false; } id = crm_element_value(xml_obj, attr); if (id == NULL) { return true; } if (!pcmk__valid_resource_or_tag(scheduler, id, &rsc, &tag)) { pcmk__config_err("Ignoring constraint '%s' because '%s' is not a " "valid resource or tag", cons_id, id); return false; } else if (tag) { /* The "attr" attribute (for a resource in a constraint) specifies a * template or tag. Add the corresponding PCMK_XE_RESOURCE_SET * containing the resources derived from or tagged with it. */ *rsc_set = pcmk__xe_create(xml_obj, PCMK_XE_RESOURCE_SET); crm_xml_add(*rsc_set, PCMK_XA_ID, id); for (GList *iter = tag->refs; iter != NULL; iter = iter->next) { const char *obj_ref = iter->data; xmlNode *rsc_ref = NULL; rsc_ref = pcmk__xe_create(*rsc_set, PCMK_XE_RESOURCE_REF); crm_xml_add(rsc_ref, PCMK_XA_ID, obj_ref); } // Set PCMK_XA_SEQUENTIAL=PCMK_VALUE_FALSE for the PCMK_XE_RESOURCE_SET pcmk__xe_set_bool_attr(*rsc_set, PCMK_XA_SEQUENTIAL, false); } else if ((rsc != NULL) && convert_rsc) { /* Even if a regular resource is referenced by "attr", convert it into a * PCMK_XE_RESOURCE_SET, because the other resource reference in the * constraint could be a template or tag. */ xmlNode *rsc_ref = NULL; *rsc_set = pcmk__xe_create(xml_obj, PCMK_XE_RESOURCE_SET); crm_xml_add(*rsc_set, PCMK_XA_ID, id); rsc_ref = pcmk__xe_create(*rsc_set, PCMK_XE_RESOURCE_REF); crm_xml_add(rsc_ref, PCMK_XA_ID, id); } else { return true; } /* Remove the "attr" attribute referencing the template/tag */ if (*rsc_set != NULL) { pcmk__xe_remove_attr(xml_obj, attr); } return true; } /*! * \internal * \brief Create constraints inherent to resource types * * \param[in,out] scheduler Scheduler data */ void pcmk__create_internal_constraints(pcmk_scheduler_t *scheduler) { crm_trace("Create internal constraints"); for (GList *iter = scheduler->priv->resources; iter != NULL; iter = iter->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) iter->data; rsc->priv->cmds->internal_constraints(rsc); } } diff --git a/tools/crm_resource_print.c b/tools/crm_resource_print.c index fe962239fc..8ed55d91ea 100644 --- a/tools/crm_resource_print.c +++ b/tools/crm_resource_print.c @@ -1,927 +1,927 @@ /* * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU General Public License version 2 * or later (GPLv2+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #define cons_string(x) x?x:"NA" static int print_constraint(xmlNode *xml_obj, void *userdata) { pcmk_scheduler_t *scheduler = (pcmk_scheduler_t *) userdata; pcmk__output_t *out = scheduler->priv->out; xmlNode *lifetime = NULL; const char *id = crm_element_value(xml_obj, PCMK_XA_ID); pcmk_rule_input_t rule_input = { .now = scheduler->priv->now, }; if (id == NULL) { return pcmk_rc_ok; } // @COMPAT PCMK__XE_LIFETIME is deprecated lifetime = pcmk__xe_first_child(xml_obj, PCMK__XE_LIFETIME, NULL, NULL); if (pcmk__evaluate_rules(lifetime, &rule_input, NULL) != pcmk_rc_ok) { return pcmk_rc_ok; } if (!pcmk__xe_is(xml_obj, PCMK_XE_RSC_COLOCATION)) { return pcmk_rc_ok; } out->info(out, "Constraint %s %s %s %s %s %s %s", xml_obj->name, cons_string(crm_element_value(xml_obj, PCMK_XA_ID)), cons_string(crm_element_value(xml_obj, PCMK_XA_RSC)), cons_string(crm_element_value(xml_obj, PCMK_XA_WITH_RSC)), cons_string(crm_element_value(xml_obj, PCMK_XA_SCORE)), cons_string(crm_element_value(xml_obj, PCMK_XA_RSC_ROLE)), cons_string(crm_element_value(xml_obj, PCMK_XA_WITH_RSC_ROLE))); return pcmk_rc_ok; } void cli_resource_print_cts_constraints(pcmk_scheduler_t *scheduler) { pcmk__xe_foreach_child(pcmk_find_cib_element(scheduler->input, PCMK_XE_CONSTRAINTS), NULL, print_constraint, scheduler); } void cli_resource_print_cts(pcmk_resource_t *rsc, pcmk__output_t *out) { const char *host = NULL; bool needs_quorum = TRUE; const char *rtype = crm_element_value(rsc->priv->xml, PCMK_XA_TYPE); const char *rprov = crm_element_value(rsc->priv->xml, PCMK_XA_PROVIDER); const char *rclass = crm_element_value(rsc->priv->xml, PCMK_XA_CLASS); pcmk_node_t *node = pcmk__current_node(rsc); if (pcmk_is_set(rsc->flags, pcmk__rsc_fence_device)) { needs_quorum = FALSE; } else { // @TODO check requires in resource meta-data and rsc_defaults } if (node != NULL) { host = node->priv->name; } out->info(out, "Resource: %s %s %s %s %s %s %s %s %d %lld %#.16llx", rsc->priv->xml->name, rsc->id, pcmk__s(rsc->priv->history_id, rsc->id), ((rsc->priv->parent == NULL)? "NA" : rsc->priv->parent->id), rprov ? rprov : "NA", rclass, rtype, host ? host : "NA", needs_quorum, rsc->flags, rsc->flags); g_list_foreach(rsc->priv->children, (GFunc) cli_resource_print_cts, out); } // \return Standard Pacemaker return code int cli_resource_print_operations(const char *rsc_id, const char *host_uname, bool active, pcmk_scheduler_t *scheduler) { pcmk__output_t *out = scheduler->priv->out; int rc = pcmk_rc_no_output; GList *ops = find_operations(rsc_id, host_uname, active, scheduler); if (!ops) { return rc; } out->begin_list(out, NULL, NULL, "Resource Operations"); rc = pcmk_rc_ok; for (GList *lpc = ops; lpc != NULL; lpc = lpc->next) { xmlNode *xml_op = (xmlNode *) lpc->data; out->message(out, "node-and-op", scheduler, xml_op); } out->end_list(out); return rc; } // \return Standard Pacemaker return code int cli_resource_print(pcmk_resource_t *rsc, pcmk_scheduler_t *scheduler, bool expanded) { pcmk__output_t *out = scheduler->priv->out; uint32_t show_opts = pcmk_show_pending; GList *all = NULL; all = g_list_prepend(all, (gpointer) "*"); out->begin_list(out, NULL, NULL, "Resource Config"); out->message(out, pcmk__map_element_name(rsc->priv->xml), show_opts, rsc, all, all); out->message(out, "resource-config", rsc, !expanded); out->end_list(out); g_list_free(all); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("attribute-changed", "attr_update_data_t *") static int attribute_changed_default(pcmk__output_t *out, va_list args) { attr_update_data_t *ud = va_arg(args, attr_update_data_t *); out->info(out, "Set '%s' option: " PCMK_XA_ID "=%s%s%s%s%s value=%s", ud->given_rsc_id, ud->found_attr_id, ((ud->attr_set_id == NULL)? "" : " " PCMK__XA_SET "="), pcmk__s(ud->attr_set_id, ""), ((ud->attr_name == NULL)? "" : " " PCMK_XA_NAME "="), pcmk__s(ud->attr_name, ""), ud->attr_value); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("attribute-changed", "attr_update_data_t *") static int attribute_changed_xml(pcmk__output_t *out, va_list args) { attr_update_data_t *ud = va_arg(args, attr_update_data_t *); pcmk__output_xml_create_parent(out, (const char *) ud->rsc->priv->xml->name, PCMK_XA_ID, ud->rsc->id, NULL); pcmk__output_xml_create_parent(out, ud->attr_set_type, PCMK_XA_ID, ud->attr_set_id, NULL); pcmk__output_create_xml_node(out, PCMK_XE_NVPAIR, PCMK_XA_ID, ud->found_attr_id, PCMK_XA_VALUE, ud->attr_value, PCMK_XA_NAME, ud->attr_name, NULL); pcmk__output_xml_pop_parent(out); pcmk__output_xml_pop_parent(out); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("attribute-changed-list", "GList *") static int attribute_changed_list_default(pcmk__output_t *out, va_list args) { GList *results = va_arg(args, GList *); if (results == NULL) { return pcmk_rc_no_output; } for (GList *iter = results; iter != NULL; iter = iter->next) { attr_update_data_t *ud = iter->data; out->message(out, "attribute-changed", ud); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("attribute-changed-list", "GList *") static int attribute_changed_list_xml(pcmk__output_t *out, va_list args) { GList *results = va_arg(args, GList *); if (results == NULL) { return pcmk_rc_no_output; } pcmk__output_xml_create_parent(out, PCMK__XE_RESOURCE_SETTINGS, NULL); for (GList *iter = results; iter != NULL; iter = iter->next) { attr_update_data_t *ud = iter->data; out->message(out, "attribute-changed", ud); } pcmk__output_xml_pop_parent(out); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("attribute-list", "pcmk_resource_t *", "const char *", "const char *") static int attribute_list_default(pcmk__output_t *out, va_list args) { pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *); const char *attr = va_arg(args, char *); const char *value = va_arg(args, const char *); if (value != NULL) { out->begin_list(out, NULL, NULL, "Attributes"); out->list_item(out, attr, "%s", value); out->end_list(out); return pcmk_rc_ok; } else { out->err(out, "Attribute '%s' not found for '%s'", attr, rsc->id); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("agent-status", "int", "const char *", "const char *", "const char *", "const char *", "const char *", "crm_exit_t", "const char *") static int agent_status_default(pcmk__output_t *out, va_list args) { int status = va_arg(args, int); const char *action = va_arg(args, const char *); const char *name = va_arg(args, const char *); const char *class = va_arg(args, const char *); const char *provider = va_arg(args, const char *); const char *type = va_arg(args, const char *); crm_exit_t rc = va_arg(args, crm_exit_t); const char *exit_reason = va_arg(args, const char *); if (status == PCMK_EXEC_DONE) { /* Operation [for ] ([:]:) * returned ([: ]) */ out->info(out, "Operation %s%s%s (%s%s%s:%s) returned %d (%s%s%s)", action, ((name == NULL)? "" : " for "), ((name == NULL)? "" : name), class, ((provider == NULL)? "" : ":"), ((provider == NULL)? "" : provider), type, (int) rc, services_ocf_exitcode_str((int) rc), ((exit_reason == NULL)? "" : ": "), ((exit_reason == NULL)? "" : exit_reason)); } else { /* Operation [for ] ([:]:) * could not be executed ([: ]) */ out->err(out, "Operation %s%s%s (%s%s%s:%s) could not be executed (%s%s%s)", action, ((name == NULL)? "" : " for "), ((name == NULL)? "" : name), class, ((provider == NULL)? "" : ":"), ((provider == NULL)? "" : provider), type, pcmk_exec_status_str(status), ((exit_reason == NULL)? "" : ": "), ((exit_reason == NULL)? "" : exit_reason)); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("agent-status", "int", "const char *", "const char *", "const char *", "const char *", "const char *", "crm_exit_t", "const char *") static int agent_status_xml(pcmk__output_t *out, va_list args) { int status = va_arg(args, int); const char *action G_GNUC_UNUSED = va_arg(args, const char *); const char *name G_GNUC_UNUSED = va_arg(args, const char *); const char *class G_GNUC_UNUSED = va_arg(args, const char *); const char *provider G_GNUC_UNUSED = va_arg(args, const char *); const char *type G_GNUC_UNUSED = va_arg(args, const char *); crm_exit_t rc = va_arg(args, crm_exit_t); const char *exit_reason = va_arg(args, const char *); char *exit_s = pcmk__itoa(rc); const char *message = services_ocf_exitcode_str((int) rc); char *status_s = pcmk__itoa(status); const char *execution_message = pcmk_exec_status_str(status); pcmk__output_create_xml_node(out, PCMK_XE_AGENT_STATUS, PCMK_XA_CODE, exit_s, PCMK_XA_MESSAGE, message, PCMK_XA_EXECUTION_CODE, status_s, PCMK_XA_EXECUTION_MESSAGE, execution_message, PCMK_XA_REASON, exit_reason, NULL); free(exit_s); free(status_s); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("attribute-list", "pcmk_resource_t *", "const char *", "const char *") static int attribute_list_text(pcmk__output_t *out, va_list args) { pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *); const char *attr = va_arg(args, char *); const char *value = va_arg(args, const char *); if (value != NULL) { pcmk__formatted_printf(out, "%s\n", value); return pcmk_rc_ok; } else { out->err(out, "Attribute '%s' not found for '%s'", attr, rsc->id); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("override", "const char *", "const char *", "const char *") static int override_default(pcmk__output_t *out, va_list args) { const char *rsc_name = va_arg(args, const char *); const char *name = va_arg(args, const char *); const char *value = va_arg(args, const char *); if (rsc_name == NULL) { out->list_item(out, NULL, "Overriding the cluster configuration with '%s' = '%s'", name, value); } else { out->list_item(out, NULL, "Overriding the cluster configuration for '%s' with '%s' = '%s'", rsc_name, name, value); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("override", "const char *", "const char *", "const char *") static int override_xml(pcmk__output_t *out, va_list args) { const char *rsc_name = va_arg(args, const char *); const char *name = va_arg(args, const char *); const char *value = va_arg(args, const char *); xmlNodePtr node = pcmk__output_create_xml_node(out, PCMK_XE_OVERRIDE, PCMK_XA_NAME, name, PCMK_XA_VALUE, value, NULL); if (rsc_name != NULL) { crm_xml_add(node, PCMK_XA_RSC, rsc_name); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("property-list", "pcmk_resource_t *", "const char *") static int property_list_default(pcmk__output_t *out, va_list args) { pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *); const char *attr = va_arg(args, char *); const char *value = crm_element_value(rsc->priv->xml, attr); if (value != NULL) { out->begin_list(out, NULL, NULL, "Properties"); out->list_item(out, attr, "%s", value); out->end_list(out); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("property-list", "pcmk_resource_t *", "const char *") static int property_list_text(pcmk__output_t *out, va_list args) { pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *); const char *attr = va_arg(args, const char *); const char *value = crm_element_value(rsc->priv->xml, attr); if (value != NULL) { pcmk__formatted_printf(out, "%s\n", value); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("resource-agent-action", "int", "const char *", "const char *", "const char *", "const char *", "const char *", "GHashTable *", "crm_exit_t", "int", "const char *", "const char *", "const char *") static int resource_agent_action_default(pcmk__output_t *out, va_list args) { int verbose = va_arg(args, int); const char *class = va_arg(args, const char *); const char *provider = va_arg(args, const char *); const char *type = va_arg(args, const char *); const char *rsc_name = va_arg(args, const char *); const char *action = va_arg(args, const char *); GHashTable *overrides = va_arg(args, GHashTable *); crm_exit_t rc = va_arg(args, crm_exit_t); int status = va_arg(args, int); const char *exit_reason = va_arg(args, const char *); const char *stdout_data = va_arg(args, const char *); const char *stderr_data = va_arg(args, const char *); if (overrides) { GHashTableIter iter; const char *name = NULL; const char *value = NULL; out->begin_list(out, NULL, NULL, PCMK_XE_OVERRIDES); g_hash_table_iter_init(&iter, overrides); while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &value)) { out->message(out, "override", rsc_name, name, value); } out->end_list(out); } out->message(out, "agent-status", status, action, rsc_name, class, provider, type, rc, exit_reason); /* hide output for validate-all if not in verbose */ if ((verbose == 0) && pcmk__str_eq(action, PCMK_ACTION_VALIDATE_ALL, pcmk__str_casei)) { return pcmk_rc_ok; } if (stdout_data || stderr_data) { xmlNodePtr doc = NULL; if (stdout_data != NULL) { doc = pcmk__xml_parse(stdout_data); } if (doc != NULL) { out->output_xml(out, PCMK_XE_COMMAND, stdout_data); - xmlFreeNode(doc); + pcmk__xml_free(doc); } else { out->subprocess_output(out, rc, stdout_data, stderr_data); } } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("resource-agent-action", "int", "const char *", "const char *", "const char *", "const char *", "const char *", "GHashTable *", "crm_exit_t", "int", "const char *", "const char *", "const char *") static int resource_agent_action_xml(pcmk__output_t *out, va_list args) { int verbose G_GNUC_UNUSED = va_arg(args, int); const char *class = va_arg(args, const char *); const char *provider = va_arg(args, const char *); const char *type = va_arg(args, const char *); const char *rsc_name = va_arg(args, const char *); const char *action = va_arg(args, const char *); GHashTable *overrides = va_arg(args, GHashTable *); crm_exit_t rc = va_arg(args, crm_exit_t); int status = va_arg(args, int); const char *exit_reason = va_arg(args, const char *); const char *stdout_data = va_arg(args, const char *); const char *stderr_data = va_arg(args, const char *); xmlNodePtr node = NULL; node = pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCE_AGENT_ACTION, PCMK_XA_ACTION, action, PCMK_XA_CLASS, class, PCMK_XA_TYPE, type, NULL); if (rsc_name) { crm_xml_add(node, PCMK_XA_RSC, rsc_name); } crm_xml_add(node, PCMK_XA_PROVIDER, provider); if (overrides) { GHashTableIter iter; const char *name = NULL; const char *value = NULL; out->begin_list(out, NULL, NULL, PCMK_XE_OVERRIDES); g_hash_table_iter_init(&iter, overrides); while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &value)) { out->message(out, "override", rsc_name, name, value); } out->end_list(out); } out->message(out, "agent-status", status, action, rsc_name, class, provider, type, rc, exit_reason); if (stdout_data || stderr_data) { xmlNodePtr doc = NULL; if (stdout_data != NULL) { doc = pcmk__xml_parse(stdout_data); } if (doc != NULL) { out->output_xml(out, PCMK_XE_COMMAND, stdout_data); - xmlFreeNode(doc); + pcmk__xml_free(doc); } else { out->subprocess_output(out, rc, stdout_data, stderr_data); } } pcmk__output_xml_pop_parent(out); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("resource-check-list", "resource_checks_t *") static int resource_check_list_default(pcmk__output_t *out, va_list args) { resource_checks_t *checks = va_arg(args, resource_checks_t *); const pcmk_resource_t *parent = pe__const_top_resource(checks->rsc, false); const pcmk_scheduler_t *scheduler = checks->rsc->priv->scheduler; if (checks->flags == 0) { return pcmk_rc_no_output; } out->begin_list(out, NULL, NULL, "Resource Checks"); if (pcmk_is_set(checks->flags, rsc_remain_stopped)) { out->list_item(out, "check", "Configuration specifies '%s' should remain stopped", parent->id); } if (pcmk_is_set(checks->flags, rsc_unpromotable)) { out->list_item(out, "check", "Configuration specifies '%s' should not be promoted", parent->id); } if (pcmk_is_set(checks->flags, rsc_unmanaged)) { out->list_item(out, "check", "Configuration prevents cluster from stopping or starting unmanaged '%s'", parent->id); } if (pcmk_is_set(checks->flags, rsc_locked)) { out->list_item(out, "check", "'%s' is locked to node %s due to shutdown", parent->id, checks->lock_node); } if (pcmk_is_set(checks->flags, rsc_node_health)) { out->list_item(out, "check", "'%s' cannot run on unhealthy nodes due to " PCMK_OPT_NODE_HEALTH_STRATEGY "='%s'", parent->id, pcmk__cluster_option(scheduler->priv->options, PCMK_OPT_NODE_HEALTH_STRATEGY)); } out->end_list(out); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("resource-check-list", "resource_checks_t *") static int resource_check_list_xml(pcmk__output_t *out, va_list args) { resource_checks_t *checks = va_arg(args, resource_checks_t *); const pcmk_resource_t *parent = pe__const_top_resource(checks->rsc, false); xmlNodePtr node = pcmk__output_create_xml_node(out, PCMK_XE_CHECK, PCMK_XA_ID, parent->id, NULL); if (pcmk_is_set(checks->flags, rsc_remain_stopped)) { pcmk__xe_set_bool_attr(node, PCMK_XA_REMAIN_STOPPED, true); } if (pcmk_is_set(checks->flags, rsc_unpromotable)) { pcmk__xe_set_bool_attr(node, PCMK_XA_PROMOTABLE, false); } if (pcmk_is_set(checks->flags, rsc_unmanaged)) { pcmk__xe_set_bool_attr(node, PCMK_XA_UNMANAGED, true); } if (pcmk_is_set(checks->flags, rsc_locked)) { crm_xml_add(node, PCMK_XA_LOCKED_TO_HYPHEN, checks->lock_node); } if (pcmk_is_set(checks->flags, rsc_node_health)) { pcmk__xe_set_bool_attr(node, PCMK_XA_UNHEALTHY, true); } return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("resource-search-list", "GList *", "const gchar *") static int resource_search_list_default(pcmk__output_t *out, va_list args) { GList *nodes = va_arg(args, GList *); const gchar *requested_name = va_arg(args, const gchar *); bool printed = false; int rc = pcmk_rc_no_output; if (!out->is_quiet(out) && nodes == NULL) { out->err(out, "resource %s is NOT running", requested_name); return rc; } for (GList *lpc = nodes; lpc != NULL; lpc = lpc->next) { node_info_t *ni = (node_info_t *) lpc->data; if (!printed) { out->begin_list(out, NULL, NULL, "Nodes"); printed = true; rc = pcmk_rc_ok; } if (out->is_quiet(out)) { out->list_item(out, "node", "%s", ni->node_name); } else { const char *role_text = ""; if (ni->promoted) { role_text = " " PCMK_ROLE_PROMOTED; } out->list_item(out, "node", "resource %s is running on: %s%s", requested_name, ni->node_name, role_text); } } if (printed) { out->end_list(out); } return rc; } PCMK__OUTPUT_ARGS("resource-search-list", "GList *", "const gchar *") static int resource_search_list_xml(pcmk__output_t *out, va_list args) { GList *nodes = va_arg(args, GList *); const gchar *requested_name = va_arg(args, const gchar *); pcmk__output_xml_create_parent(out, PCMK_XE_NODES, PCMK_XA_RESOURCE, requested_name, NULL); for (GList *lpc = nodes; lpc != NULL; lpc = lpc->next) { node_info_t *ni = (node_info_t *) lpc->data; xmlNodePtr sub_node = pcmk__output_create_xml_text_node(out, PCMK_XE_NODE, ni->node_name); if (ni->promoted) { crm_xml_add(sub_node, PCMK_XA_STATE, "promoted"); } } pcmk__output_xml_pop_parent(out); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("resource-reasons-list", "GList *", "pcmk_resource_t *", "pcmk_node_t *") static int resource_reasons_list_default(pcmk__output_t *out, va_list args) { GList *resources = va_arg(args, GList *); pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *); pcmk_node_t *node = va_arg(args, pcmk_node_t *); const char *host_uname = (node == NULL)? NULL : node->priv->name; out->begin_list(out, NULL, NULL, "Resource Reasons"); if ((rsc == NULL) && (host_uname == NULL)) { GList *lpc = NULL; GList *hosts = NULL; for (lpc = resources; lpc != NULL; lpc = lpc->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) lpc->data; rsc->priv->fns->location(rsc, &hosts, pcmk__rsc_node_current); if (hosts == NULL) { out->list_item(out, "reason", "Resource %s is not running", rsc->id); } else { out->list_item(out, "reason", "Resource %s is running", rsc->id); } cli_resource_check(out, rsc, NULL); g_list_free(hosts); hosts = NULL; } } else if ((rsc != NULL) && (host_uname != NULL)) { if (resource_is_running_on(rsc, host_uname)) { out->list_item(out, "reason", "Resource %s is running on host %s", rsc->id, host_uname); } else { out->list_item(out, "reason", "Resource %s is not running on host %s", rsc->id, host_uname); } cli_resource_check(out, rsc, node); } else if ((rsc == NULL) && (host_uname != NULL)) { const char* host_uname = node->priv->name; GList *allResources = node->priv->assigned_resources; GList *activeResources = node->details->running_rsc; GList *unactiveResources = pcmk__subtract_lists(allResources, activeResources, (GCompareFunc) strcmp); GList *lpc = NULL; for (lpc = activeResources; lpc != NULL; lpc = lpc->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) lpc->data; out->list_item(out, "reason", "Resource %s is running on host %s", rsc->id, host_uname); cli_resource_check(out, rsc, node); } for(lpc = unactiveResources; lpc != NULL; lpc = lpc->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) lpc->data; out->list_item(out, "reason", "Resource %s is assigned to host %s but not running", rsc->id, host_uname); cli_resource_check(out, rsc, node); } g_list_free(allResources); g_list_free(activeResources); g_list_free(unactiveResources); } else if ((rsc != NULL) && (host_uname == NULL)) { GList *hosts = NULL; rsc->priv->fns->location(rsc, &hosts, pcmk__rsc_node_current); out->list_item(out, "reason", "Resource %s is %srunning", rsc->id, (hosts? "" : "not ")); cli_resource_check(out, rsc, NULL); g_list_free(hosts); } out->end_list(out); return pcmk_rc_ok; } PCMK__OUTPUT_ARGS("resource-reasons-list", "GList *", "pcmk_resource_t *", "pcmk_node_t *") static int resource_reasons_list_xml(pcmk__output_t *out, va_list args) { GList *resources = va_arg(args, GList *); pcmk_resource_t *rsc = va_arg(args, pcmk_resource_t *); pcmk_node_t *node = va_arg(args, pcmk_node_t *); const char *host_uname = (node == NULL)? NULL : node->priv->name; xmlNodePtr xml_node = pcmk__output_xml_create_parent(out, PCMK_XE_REASON, NULL); if ((rsc == NULL) && (host_uname == NULL)) { GList *lpc = NULL; GList *hosts = NULL; pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCES, NULL); for (lpc = resources; lpc != NULL; lpc = lpc->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) lpc->data; const char *running = NULL; rsc->priv->fns->location(rsc, &hosts, pcmk__rsc_node_current); running = pcmk__btoa(hosts != NULL); pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCE, PCMK_XA_ID, rsc->id, PCMK_XA_RUNNING, running, NULL); cli_resource_check(out, rsc, NULL); pcmk__output_xml_pop_parent(out); g_list_free(hosts); hosts = NULL; } pcmk__output_xml_pop_parent(out); } else if ((rsc != NULL) && (host_uname != NULL)) { if (resource_is_running_on(rsc, host_uname)) { crm_xml_add(xml_node, PCMK_XA_RUNNING_ON, host_uname); } cli_resource_check(out, rsc, node); } else if ((rsc == NULL) && (host_uname != NULL)) { const char* host_uname = node->priv->name; GList *allResources = node->priv->assigned_resources; GList *activeResources = node->details->running_rsc; GList *unactiveResources = pcmk__subtract_lists(allResources, activeResources, (GCompareFunc) strcmp); GList *lpc = NULL; pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCES, NULL); for (lpc = activeResources; lpc != NULL; lpc = lpc->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) lpc->data; pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCE, PCMK_XA_ID, rsc->id, PCMK_XA_RUNNING, PCMK_VALUE_TRUE, PCMK_XA_HOST, host_uname, NULL); cli_resource_check(out, rsc, node); pcmk__output_xml_pop_parent(out); } for(lpc = unactiveResources; lpc != NULL; lpc = lpc->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) lpc->data; pcmk__output_xml_create_parent(out, PCMK_XE_RESOURCE, PCMK_XA_ID, rsc->id, PCMK_XA_RUNNING, PCMK_VALUE_FALSE, PCMK_XA_HOST, host_uname, NULL); cli_resource_check(out, rsc, node); pcmk__output_xml_pop_parent(out); } pcmk__output_xml_pop_parent(out); g_list_free(allResources); g_list_free(activeResources); g_list_free(unactiveResources); } else if ((rsc != NULL) && (host_uname == NULL)) { GList *hosts = NULL; rsc->priv->fns->location(rsc, &hosts, pcmk__rsc_node_current); crm_xml_add(xml_node, PCMK_XA_RUNNING, pcmk__btoa(hosts != NULL)); cli_resource_check(out, rsc, NULL); g_list_free(hosts); } pcmk__output_xml_pop_parent(out); return pcmk_rc_ok; } static void add_resource_name(pcmk_resource_t *rsc, pcmk__output_t *out) { if (rsc->priv->children == NULL) { /* Sometimes PCMK_XE_RESOURCE might act as a PCMK_XA_NAME instead of an * XML element name, depending on whether pcmk__output_enable_list_element * was called. */ out->list_item(out, PCMK_XE_RESOURCE, "%s", rsc->id); } else { g_list_foreach(rsc->priv->children, (GFunc) add_resource_name, out); } } PCMK__OUTPUT_ARGS("resource-names-list", "GList *") static int resource_names(pcmk__output_t *out, va_list args) { GList *resources = va_arg(args, GList *); if (resources == NULL) { out->err(out, "NO resources configured\n"); return pcmk_rc_no_output; } out->begin_list(out, NULL, NULL, "Resource Names"); g_list_foreach(resources, (GFunc) add_resource_name, out); out->end_list(out); return pcmk_rc_ok; } static pcmk__message_entry_t fmt_functions[] = { { "agent-status", "default", agent_status_default }, { "agent-status", "xml", agent_status_xml }, { "attribute-changed", "default", attribute_changed_default }, { "attribute-changed", "xml", attribute_changed_xml }, { "attribute-changed-list", "default", attribute_changed_list_default }, { "attribute-changed-list", "xml", attribute_changed_list_xml }, { "attribute-list", "default", attribute_list_default }, { "attribute-list", "text", attribute_list_text }, { "override", "default", override_default }, { "override", "xml", override_xml }, { "property-list", "default", property_list_default }, { "property-list", "text", property_list_text }, { "resource-agent-action", "default", resource_agent_action_default }, { "resource-agent-action", "xml", resource_agent_action_xml }, { "resource-check-list", "default", resource_check_list_default }, { "resource-check-list", "xml", resource_check_list_xml }, { "resource-search-list", "default", resource_search_list_default }, { "resource-search-list", "xml", resource_search_list_xml }, { "resource-reasons-list", "default", resource_reasons_list_default }, { "resource-reasons-list", "xml", resource_reasons_list_xml }, { "resource-names-list", "default", resource_names }, { NULL, NULL, NULL } }; void crm_resource_register_messages(pcmk__output_t *out) { pcmk__register_messages(out, fmt_functions); }