Blob Blame History Raw
From b221d83628cf1413abfa5d836c103a94184b3c46 Mon Sep 17 00:00:00 2001
From: Tomas Jelinek <tojeline@redhat.com>
Date: Fri, 22 Jul 2016 12:06:24 +0200
Subject: [PATCH] improve node properties commands

* added "pcs node attribute" command
* allow to list value of specified attribute / utilization from all nodes
---
 pcs/node.py           |  64 +++++++++---
 pcs/pcs.8             |   7 +-
 pcs/prop.py           |  13 ++-
 pcs/test/test_node.py | 278 ++++++++++++++++++++++++++++++++++++++++++++++++--
 pcs/usage.py          |  21 ++--
 pcs/utils.py          |  13 ++-
 6 files changed, 355 insertions(+), 41 deletions(-)

diff --git a/pcs/node.py b/pcs/node.py
index ac154d4..be2fb13 100644
--- a/pcs/node.py
+++ b/pcs/node.py
@@ -12,6 +12,8 @@ from pcs import (
     usage,
     utils,
 )
+from pcs.cli.common.errors import CmdLineInputError
+from pcs.cli.common.parse_args import prepare_options
 from pcs.lib.errors import LibraryError
 import pcs.lib.pacemaker as lib_pacemaker
 from pcs.lib.pacemaker_values import get_valid_timeout_seconds
@@ -33,11 +35,26 @@ def node_cmd(argv):
         node_standby(argv)
     elif sub_cmd == "unstandby":
         node_standby(argv, False)
+    elif sub_cmd == "attribute":
+        if "--name" in utils.pcs_options and len(argv) > 1:
+            usage.node("attribute")
+            sys.exit(1)
+        filter_attr=utils.pcs_options.get("--name", None)
+        if len(argv) == 0:
+            attribute_show_cmd(filter_attr=filter_attr)
+        elif len(argv) == 1:
+            attribute_show_cmd(argv.pop(0), filter_attr=filter_attr)
+        else:
+            attribute_set_cmd(argv.pop(0), argv)
     elif sub_cmd == "utilization":
+        if "--name" in utils.pcs_options and len(argv) > 1:
+            usage.node("utilization")
+            sys.exit(1)
+        filter_name=utils.pcs_options.get("--name", None)
         if len(argv) == 0:
-            print_nodes_utilization()
+            print_node_utilization(filter_name=filter_name)
         elif len(argv) == 1:
-            print_node_utilization(argv.pop(0))
+            print_node_utilization(argv.pop(0), filter_name=filter_name)
         else:
             set_node_utilization(argv.pop(0), argv)
     # pcs-to-pcsd use only
@@ -135,23 +152,16 @@ def set_node_utilization(node, argv):
     )
     utils.replace_cib_configuration(cib)
 
-def print_node_utilization(node):
-    cib = utils.get_cib_dom()
-    node_el = utils.dom_get_node(cib, node)
-    if node_el is None:
-        utils.err("Unable to find a node: {0}".format(node))
-    utilization = utils.get_utilization_str(node_el)
-
-    print("Node Utilization:")
-    print(" {0}: {1}".format(node, utilization))
-
-def print_nodes_utilization():
+def print_node_utilization(filter_node=None, filter_name=None):
     cib = utils.get_cib_dom()
     utilization = {}
     for node_el in cib.getElementsByTagName("node"):
-        u = utils.get_utilization_str(node_el)
+        node = node_el.getAttribute("uname")
+        if filter_node is not None and node != filter_node:
+            continue
+        u = utils.get_utilization_str(node_el, filter_name)
         if u:
-            utilization[node_el.getAttribute("uname")] = u
+            utilization[node] = u
     print("Node Utilization:")
     for node in sorted(utilization):
         print(" {0}: {1}".format(node, utilization[node]))
@@ -163,3 +173,27 @@ def node_pacemaker_status():
         ))
     except LibraryError as e:
         utils.process_library_reports(e.args)
+
+def attribute_show_cmd(filter_node=None, filter_attr=None):
+    node_attributes = utils.get_node_attributes(
+        filter_node=filter_node,
+        filter_attr=filter_attr
+    )
+    print("Node Attributes:")
+    attribute_print(node_attributes)
+
+def attribute_set_cmd(node, argv):
+    try:
+        attrs = prepare_options(argv)
+    except CmdLineInputError as e:
+        utils.exit_on_cmdline_input_errror(e, "node", "attribute")
+    for name, value in attrs.items():
+        utils.set_node_attribute(name, value, node)
+
+def attribute_print(node_attributes):
+    for node in sorted(node_attributes.keys()):
+        line_parts = [" " + node + ":"]
+        for name, value in sorted(node_attributes[node].items()):
+            line_parts.append("{0}={1}".format(name, value))
+        print(" ".join(line_parts))
+
diff --git a/pcs/pcs.8 b/pcs/pcs.8
index 16c9331..f789df7 100644
--- a/pcs/pcs.8
+++ b/pcs/pcs.8
@@ -644,6 +644,9 @@ clear-auth [\fB\-\-local\fR] [\fB\-\-remote\fR]
 Removes all system tokens which allow pcs/pcsd on the current system to authenticate with remote pcs/pcsd instances and vice\-versa.  After this command is run this node will need to be re\-authenticated with other nodes (using 'pcs cluster auth').  Using \fB\-\-local\fR only removes tokens used by local pcs (and pcsd if root) to connect to other pcsd instances, using \fB\-\-remote\fR clears authentication tokens used by remote systems to connect to the local pcsd instance.
 .SS "node"
 .TP
+attribute [[<node>] [\fB\-\-name\fR <attr>] | <node> <name>=<value> ...]
+Manage node attributes.  If no parameters are specified, show attributes of all nodes.  If one parameter is specified, show attributes of specified node.  If \fB\-\-name\fR is specified, show specified attribute's value from all nodes.  If more parameters are specified, set attributes of specified node.  Attributes can be removed by setting an attribute without a value.
+.TP
 maintenance [\fB\-\-all\fR] | [<node>]...
 Put specified node(s) into maintenance mode, if no node or options are specified the current node will be put into maintenance mode, if \fB\-\-all\fR is specified all nodes will be put into maintenace mode.
 .TP
@@ -656,8 +659,8 @@ Put specified node into standby mode (the node specified will no longer be able
 unstandby [\fB\-\-all\fR | <node>] [\fB\-\-wait\fR[=n]]
 Remove node from standby mode (the node specified will now be able to host resources), if no node or options are specified the current node will be removed from standby mode, if \fB\-\-all\fR is specified all nodes will be removed from standby mode.  If \fB\-\-wait\fR is specified, pcs will wait up to 'n' seconds for the node(s) to be removed from standby mode and then return 0 on success or 1 if the operation not succeeded yet.  If 'n' is not specified it defaults to 60 minutes.
 .TP
-utilization [<node> [<name>=<value> ...]]
-Add specified utilization options to specified node. If node is not specified, shows utilization of all nodes. If utilization options are not specified, shows utilization of specified node. Utilization option should be in format name=value, value has to be integer. Options may be removed by setting an option without a value. Example: pcs node utilization node1 cpu=4 ram=
+utilization [[<node>] [\fB\-\-name\fR <name>] | <node> <name>=<value> ...]
+Add specified utilization options to specified node.  If node is not specified, shows utilization of all nodes.  If \fB\-\-name\fR is specified, shows specified utilization value from all nodes. If utilization options are not specified, shows utilization of specified node.  Utilization option should be in format name=value, value has to be integer.  Options may be removed by setting an option without a value.  Example: pcs node utilization node1 cpu=4 ram=
 .SS "alert"
 .TP
 [config|show]
diff --git a/pcs/prop.py b/pcs/prop.py
index 92a953c..1089865 100644
--- a/pcs/prop.py
+++ b/pcs/prop.py
@@ -8,8 +8,11 @@ from __future__ import (
 import sys
 import json
 
-from pcs import usage
-from pcs import utils
+from pcs import (
+    node,
+    usage,
+    utils,
+)
 
 def property_cmd(argv):
     if len(argv) == 0:
@@ -127,11 +130,7 @@ def list_property(argv):
     )
     if node_attributes:
         print("Node Attributes:")
-        for node in sorted(node_attributes.keys()):
-            line_parts = [" " + node + ":"]
-            for name, value in sorted(node_attributes[node].items()):
-                line_parts.append("{0}={1}".format(name, value))
-            print(" ".join(line_parts))
+        node.attribute_print(node_attributes)
 
 def get_default_properties():
     parameters = {}
diff --git a/pcs/test/test_node.py b/pcs/test/test_node.py
index 023148c..6f03112 100644
--- a/pcs/test/test_node.py
+++ b/pcs/test/test_node.py
@@ -8,11 +8,17 @@ from __future__ import (
 import shutil
 import unittest
 
+from pcs.test.tools.assertions import AssertPcsMixin
 from pcs.test.tools.misc import (
     ac,
     get_test_resource as rc,
 )
-from pcs.test.tools.pcs_runner import pcs
+from pcs.test.tools.pcs_runner import (
+    pcs,
+    PcsRunner,
+)
+
+from pcs import utils
 
 empty_cib = rc("cib-empty-withnodes.xml")
 temp_cib = rc("temp-cib.xml")
@@ -182,7 +188,7 @@ Cluster Properties:
         output, returnVal = pcs(temp_cib, "node utilization rh7-2")
         expected_out = """\
 Node Utilization:
- rh7-2: \n"""
+"""
         ac(expected_out, output)
         self.assertEqual(0, returnVal)
 
@@ -229,14 +235,33 @@ Node Utilization:
         ac(expected_out, output)
         self.assertEqual(0, returnVal)
 
-    def test_node_utilization_set_invalid(self):
-        output, returnVal = pcs(temp_cib, "node utilization rh7-0")
+        output, returnVal = pcs(
+            temp_cib, "node utilization rh7-2 test1=-20"
+        )
+        ac("", output)
+        self.assertEqual(0, returnVal)
+
+        output, returnVal = pcs(temp_cib, "node utilization --name test1")
         expected_out = """\
-Error: Unable to find a node: rh7-0
+Node Utilization:
+ rh7-1: test1=-10
+ rh7-2: test1=-20
 """
         ac(expected_out, output)
-        self.assertEqual(1, returnVal)
+        self.assertEqual(0, returnVal)
 
+        output, returnVal = pcs(
+            temp_cib,
+            "node utilization --name test1 rh7-2"
+        )
+        expected_out = """\
+Node Utilization:
+ rh7-2: test1=-20
+"""
+        ac(expected_out, output)
+        self.assertEqual(0, returnVal)
+
+    def test_node_utilization_set_invalid(self):
         output, returnVal = pcs(temp_cib, "node utilization rh7-0 test=10")
         expected_out = """\
 Error: Unable to find a node: rh7-0
@@ -252,3 +277,244 @@ Error: Value of utilization attribute must be integer: 'test=int'
 """
         ac(expected_out, output)
         self.assertEqual(1, returnVal)
+
+
+class NodeAttributeTest(unittest.TestCase, AssertPcsMixin):
+    def setUp(self):
+        shutil.copy(empty_cib, temp_cib)
+        self.pcs_runner = PcsRunner(temp_cib)
+
+    def fixture_attrs(self, nodes, attrs=None):
+        attrs = dict() if attrs is None else attrs
+        xml_lines = ['<nodes>']
+        for node_id, node_name in enumerate(nodes, 1):
+            xml_lines.extend([
+                '<node id="{0}" uname="{1}">'.format(node_id, node_name),
+                '<instance_attributes id="nodes-{0}">'.format(node_id),
+            ])
+            nv = '<nvpair id="nodes-{id}-{name}" name="{name}" value="{val}"/>'
+            for name, value in attrs.get(node_name, dict()).items():
+                xml_lines.append(nv.format(id=node_id, name=name, val=value))
+            xml_lines.extend([
+                '</instance_attributes>',
+                '</node>'
+            ])
+        xml_lines.append('</nodes>')
+
+        utils.usefile = True
+        utils.filename = temp_cib
+        output, retval = utils.run([
+            "cibadmin", "--modify", '--xml-text', "\n".join(xml_lines)
+        ])
+        assert output == ""
+        assert retval == 0
+
+    def test_show_empty(self):
+        self.fixture_attrs(["rh7-1", "rh7-2"])
+        self.assert_pcs_success(
+            "node attribute",
+            "Node Attributes:\n"
+        )
+
+    def test_show_nonempty(self):
+        self.fixture_attrs(
+            ["rh7-1", "rh7-2"],
+            {
+                "rh7-1": {"IP": "192.168.1.1", },
+                "rh7-2": {"IP": "192.168.1.2", },
+            }
+        )
+        self.assert_pcs_success(
+            "node attribute",
+            """\
+Node Attributes:
+ rh7-1: IP=192.168.1.1
+ rh7-2: IP=192.168.1.2
+"""
+        )
+
+    def test_show_multiple_per_node(self):
+        self.fixture_attrs(
+            ["rh7-1", "rh7-2"],
+            {
+                "rh7-1": {"IP": "192.168.1.1", "alias": "node1", },
+                "rh7-2": {"IP": "192.168.1.2", "alias": "node2", },
+            }
+        )
+        self.assert_pcs_success(
+            "node attribute",
+            """\
+Node Attributes:
+ rh7-1: IP=192.168.1.1 alias=node1
+ rh7-2: IP=192.168.1.2 alias=node2
+"""
+        )
+
+    def test_show_one_node(self):
+        self.fixture_attrs(
+            ["rh7-1", "rh7-2"],
+            {
+                "rh7-1": {"IP": "192.168.1.1", "alias": "node1", },
+                "rh7-2": {"IP": "192.168.1.2", "alias": "node2", },
+            }
+        )
+        self.assert_pcs_success(
+            "node attribute rh7-1",
+            """\
+Node Attributes:
+ rh7-1: IP=192.168.1.1 alias=node1
+"""
+        )
+
+    def test_show_missing_node(self):
+        self.fixture_attrs(
+            ["rh7-1", "rh7-2"],
+            {
+                "rh7-1": {"IP": "192.168.1.1", "alias": "node1", },
+                "rh7-2": {"IP": "192.168.1.2", "alias": "node2", },
+            }
+        )
+        self.assert_pcs_success(
+            "node attribute rh7-3",
+            """\
+Node Attributes:
+"""
+        )
+
+    def test_show_name(self):
+        self.fixture_attrs(
+            ["rh7-1", "rh7-2"],
+            {
+                "rh7-1": {"IP": "192.168.1.1", "alias": "node1", },
+                "rh7-2": {"IP": "192.168.1.2", "alias": "node2", },
+            }
+        )
+        self.assert_pcs_success(
+            "node attribute --name alias",
+            """\
+Node Attributes:
+ rh7-1: alias=node1
+ rh7-2: alias=node2
+"""
+        )
+
+    def test_show_missing_name(self):
+        self.fixture_attrs(
+            ["rh7-1", "rh7-2"],
+            {
+                "rh7-1": {"IP": "192.168.1.1", "alias": "node1", },
+                "rh7-2": {"IP": "192.168.1.2", "alias": "node2", },
+            }
+        )
+        self.assert_pcs_success(
+            "node attribute --name missing",
+            """\
+Node Attributes:
+"""
+        )
+
+    def test_show_node_and_name(self):
+        self.fixture_attrs(
+            ["rh7-1", "rh7-2"],
+            {
+                "rh7-1": {"IP": "192.168.1.1", "alias": "node1", },
+                "rh7-2": {"IP": "192.168.1.2", "alias": "node2", },
+            }
+        )
+        self.assert_pcs_success(
+            "node attribute --name alias rh7-1",
+            """\
+Node Attributes:
+ rh7-1: alias=node1
+"""
+        )
+
+    def test_set_new(self):
+        self.fixture_attrs(["rh7-1", "rh7-2"])
+        self.assert_pcs_success(
+            "node attribute rh7-1 IP=192.168.1.1"
+        )
+        self.assert_pcs_success(
+            "node attribute",
+            """\
+Node Attributes:
+ rh7-1: IP=192.168.1.1
+"""
+        )
+        self.assert_pcs_success(
+            "node attribute rh7-2 IP=192.168.1.2"
+        )
+        self.assert_pcs_success(
+            "node attribute",
+            """\
+Node Attributes:
+ rh7-1: IP=192.168.1.1
+ rh7-2: IP=192.168.1.2
+"""
+        )
+
+    def test_set_existing(self):
+        self.fixture_attrs(
+            ["rh7-1", "rh7-2"],
+            {
+                "rh7-1": {"IP": "192.168.1.1", },
+                "rh7-2": {"IP": "192.168.1.2", },
+            }
+        )
+        self.assert_pcs_success(
+            "node attribute rh7-2 IP=192.168.2.2"
+        )
+        self.assert_pcs_success(
+            "node attribute",
+            """\
+Node Attributes:
+ rh7-1: IP=192.168.1.1
+ rh7-2: IP=192.168.2.2
+"""
+        )
+
+    def test_unset(self):
+        self.fixture_attrs(
+            ["rh7-1", "rh7-2"],
+            {
+                "rh7-1": {"IP": "192.168.1.1", },
+                "rh7-2": {"IP": "192.168.1.2", },
+            }
+        )
+        self.assert_pcs_success(
+            "node attribute rh7-2 IP="
+        )
+        self.assert_pcs_success(
+            "node attribute",
+            """\
+Node Attributes:
+ rh7-1: IP=192.168.1.1
+"""
+        )
+
+    def test_unset_nonexisting(self):
+        self.fixture_attrs(
+            ["rh7-1", "rh7-2"],
+            {
+                "rh7-1": {"IP": "192.168.1.1", },
+                "rh7-2": {"IP": "192.168.1.2", },
+            }
+        )
+        self.assert_pcs_result(
+            "node attribute rh7-1 missing=",
+            "Error: attribute: 'missing' doesn't exist for node: 'rh7-1'\n",
+            returncode=2
+        )
+
+    def test_unset_nonexisting_forced(self):
+        self.fixture_attrs(
+            ["rh7-1", "rh7-2"],
+            {
+                "rh7-1": {"IP": "192.168.1.1", },
+                "rh7-2": {"IP": "192.168.1.2", },
+            }
+        )
+        self.assert_pcs_success(
+            "node attribute rh7-1 missing= --force",
+            ""
+        )
diff --git a/pcs/usage.py b/pcs/usage.py
index 0474324..2f8f855 100644
--- a/pcs/usage.py
+++ b/pcs/usage.py
@@ -1242,6 +1242,14 @@ Usage: pcs node <command>
 Manage cluster nodes
 
 Commands:
+    attribute [[<node>] [--name <name>] | <node> <name>=<value> ...]
+        Manage node attributes.  If no parameters are specified, show attributes
+        of all nodes.  If one parameter is specified, show attributes
+        of specified node.  If --name is specified, show specified attribute's
+        value from all nodes.  If more parameters are specified, set attributes
+        of specified node.  Attributes can be removed by setting an attribute
+        without a value.
+
     maintenance [--all] | [<node>]...
         Put specified node(s) into maintenance mode, if no node or options are
         specified the current node will be put into maintenance mode, if --all
@@ -1272,12 +1280,13 @@ Commands:
         the operation not succeeded yet.  If 'n' is not specified it defaults
         to 60 minutes.
 
-    utilization [<node> [<name>=<value> ...]]
-        Add specified utilization options to specified node. If node is not
-        specified, shows utilization of all nodes. If utilization options are
-        not specified, shows utilization of specified node. Utilization option
-        should be in format name=value, value has to be integer. Options may be
-        removed by setting an option without a value.
+    utilization [[<node>] [--name <name>] | <node> <name>=<value> ...]
+        Add specified utilization options to specified node.  If node is not
+        specified, shows utilization of all nodes.  If --name is specified,
+        shows specified utilization value from all nodes. If utilization options
+        are not specified, shows utilization of specified node.  Utilization
+        option should be in format name=value, value has to be integer.  Options
+        may be removed by setting an option without a value.
         Example: pcs node utilization node1 cpu=4 ram=
 """
     if pout:
diff --git a/pcs/utils.py b/pcs/utils.py
index c7d1759..079d916 100644
--- a/pcs/utils.py
+++ b/pcs/utils.py
@@ -1677,6 +1677,8 @@ def get_node_attributes(filter_node=None, filter_attr=None):
                 if nodename not in nas:
                     nas[nodename] = dict()
                 nas[nodename][attr_name] = nvp.getAttribute("value")
+            # Use just first element of attributes. We don't support
+            # attributes with rules just yet.
             break
     return nas
 
@@ -2447,21 +2449,22 @@ def dom_update_meta_attr(dom_element, attributes):
             meta_attributes.getAttribute("id") + "-"
         )
 
-def get_utilization(element):
+def get_utilization(element, filter_name=None):
     utilization = {}
     for e in element.getElementsByTagName("utilization"):
         for u in e.getElementsByTagName("nvpair"):
             name = u.getAttribute("name")
-            value = u.getAttribute("value") if u.hasAttribute("value") else ""
-            utilization[name] = value
+            if filter_name is not None and name != filter_name:
+                continue
+            utilization[name] = u.getAttribute("value")
         # Use just first element of utilization attributes. We don't support
         # utilization with rules just yet.
         break
     return utilization
 
-def get_utilization_str(element):
+def get_utilization_str(element, filter_name=None):
     output = []
-    for name, value in sorted(get_utilization(element).items()):
+    for name, value in sorted(get_utilization(element, filter_name).items()):
         output.append(name + "=" + value)
     return " ".join(output)
 
-- 
1.8.3.1