Improve error reporting in _VerifyClientCertificates

Let the user know that the issues can be fixed using
gnt-cluster renew-crypto --new-node-certificates

Signed-off-by: Brian Foley <bpfoley@google.com>
Reviewed-by: Viktor Bachraty <vbachraty@google.com>
diff --git a/lib/cmdlib/cluster/verify.py b/lib/cmdlib/cluster/verify.py
index 79fa0d8..c32820a 100644
--- a/lib/cmdlib/cluster/verify.py
+++ b/lib/cmdlib/cluster/verify.py
@@ -1015,6 +1015,11 @@
                       " should node %s fail (%dMiB needed, %dMiB available)",
                       self.cfg.GetNodeName(prinode), needed_mem, n_img.mfree)
 
+  def _CertError(self, *args):
+    """Helper function for _VerifyClientCertificates."""
+    self._Error(constants.CV_ECLUSTERCLIENTCERT, None, *args)
+    self._cert_error_found = True
+
   def _VerifyClientCertificates(self, nodes, all_nvinfo):
     """Verifies the consistency of the client certificates.
 
@@ -1029,20 +1034,25 @@
       all nodes
 
     """
-    candidate_certs = self.cfg.GetClusterInfo().candidate_certs
-    if candidate_certs is None or len(candidate_certs) == 0:
-      self._ErrorIf(
-        True, constants.CV_ECLUSTERCLIENTCERT, None,
-        "The cluster's list of master candidate certificates is empty."
-        " If you just updated the cluster, please run"
+
+    rebuild_certs_msg = (
+        "To rebuild node certificates, please run"
         " 'gnt-cluster renew-crypto --new-node-certificates'.")
+
+    self._cert_error_found = False
+
+    candidate_certs = self.cfg.GetClusterInfo().candidate_certs
+    if not candidate_certs:
+      self._CertError(
+        "The cluster's list of master candidate certificates is empty."
+        " This may be because you just updated the cluster. " +
+        rebuild_certs_msg)
       return
 
-    self._ErrorIf(
-      len(candidate_certs) != len(set(candidate_certs.values())),
-      constants.CV_ECLUSTERCLIENTCERT, None,
-      "There are at least two master candidates configured to use the same"
-      " certificate.")
+    if len(candidate_certs) != len(set(candidate_certs.values())):
+      self._CertError(
+        "There are at least two master candidates configured to use the same"
+        " certificate.")
 
     # collect the client certificate
     for node in nodes:
@@ -1055,45 +1065,42 @@
 
       (errcode, msg) = nresult.payload.get(constants.NV_CLIENT_CERT, None)
 
-      self._ErrorIf(
-        errcode is not None, constants.CV_ECLUSTERCLIENTCERT, None,
-        "Client certificate of node '%s' failed validation: %s (code '%s')",
-        node.uuid, msg, errcode)
-
+      if errcode is not None:
+        self._CertError(
+          "Client certificate of node '%s' failed validation: %s (code '%s')",
+          node.uuid, msg, errcode)
       if not errcode:
         digest = msg
         if node.master_candidate:
           if node.uuid in candidate_certs:
-            self._ErrorIf(
-              digest != candidate_certs[node.uuid],
-              constants.CV_ECLUSTERCLIENTCERT, None,
-              "Client certificate digest of master candidate '%s' does not"
-              " match its entry in the cluster's map of master candidate"
-              " certificates. Expected: %s Got: %s", node.uuid,
-              digest, candidate_certs[node.uuid])
+            if digest != candidate_certs[node.uuid]:
+              self._CertError(
+                "Client certificate digest of master candidate '%s' does not"
+                " match its entry in the cluster's map of master candidate"
+                " certificates. Expected: %s Got: %s", node.uuid,
+                digest, candidate_certs[node.uuid])
           else:
-            self._ErrorIf(
-              True, constants.CV_ECLUSTERCLIENTCERT, None,
+            self._CertError(
               "The master candidate '%s' does not have an entry in the"
               " map of candidate certificates.", node.uuid)
-            self._ErrorIf(
-              digest in candidate_certs.values(),
-              constants.CV_ECLUSTERCLIENTCERT, None,
-              "Master candidate '%s' is using a certificate of another node.",
-              node.uuid)
+            if digest in candidate_certs.values():
+              self._CertError(
+                "Master candidate '%s' is using a certificate of another node.",
+                node.uuid)
         else:
-          self._ErrorIf(
-            node.uuid in candidate_certs,
-            constants.CV_ECLUSTERCLIENTCERT, None,
-            "Node '%s' is not a master candidate, but still listed in the"
-            " map of master candidate certificates.", node.uuid)
-          self._ErrorIf(
-            (node.uuid not in candidate_certs) and
-              (digest in candidate_certs.values()),
-            constants.CV_ECLUSTERCLIENTCERT, None,
-            "Node '%s' is not a master candidate and is incorrectly using a"
-            " certificate of another node which is master candidate.",
-            node.uuid)
+          if node.uuid in candidate_certs:
+            self._CertError(
+              "Node '%s' is not a master candidate, but still listed in the"
+              " map of master candidate certificates.", node.uuid)
+          if (node.uuid not in candidate_certs and
+              digest in candidate_certs.values()):
+            self._CertError(
+              "Node '%s' is not a master candidate and is incorrectly using a"
+              " certificate of another node which is master candidate.",
+              node.uuid)
+
+    if self._cert_error_found:
+      self._CertError(rebuild_certs_msg)
 
   def _VerifySshSetup(self, nodes, all_nvinfo):
     """Evaluates the verification results of the SSH setup and clutter test.
diff --git a/test/py/cmdlib/cluster_unittest.py b/test/py/cmdlib/cluster_unittest.py
index d865dc2..22701d9 100644
--- a/test/py/cmdlib/cluster_unittest.py
+++ b/test/py/cmdlib/cluster_unittest.py
@@ -1231,8 +1231,13 @@
         .Build()
     op = opcodes.OpClusterVerifyGroup(group_name="default", verbose=True)
     self.ExecOpCode(op)
-    self.mcpu.assertLogContainsRegex("Client certificate")
-    self.mcpu.assertLogContainsRegex("failed validation")
+    regexps = (
+      "Client certificate",
+      "failed validation",
+      "gnt-cluster renew-crypto --new-node-certificates",
+    )
+    for r in regexps:
+      self.mcpu.assertLogContainsRegex(r)
 
   def testVerifyNoMasterCandidateMap(self):
     client_cert = "client-cert-digest"
@@ -1246,6 +1251,8 @@
     self.ExecOpCode(op)
     self.mcpu.assertLogContainsRegex(
       "list of master candidate certificates is empty")
+    self.mcpu.assertLogContainsRegex(
+      "gnt-cluster renew-crypto --new-node-certificates")
 
   def testVerifyNoSharingMasterCandidates(self):
     client_cert = "client-cert-digest"
@@ -1261,6 +1268,8 @@
     self.ExecOpCode(op)
     self.mcpu.assertLogContainsRegex(
       "two master candidates configured to use the same")
+    self.mcpu.assertLogContainsRegex(
+      "gnt-cluster renew-crypto --new-node-certificates")
 
   def testVerifyMasterCandidateCertMismatch(self):
     client_cert = "client-cert-digest"
@@ -1273,6 +1282,8 @@
     op = opcodes.OpClusterVerifyGroup(group_name="default", verbose=True)
     self.ExecOpCode(op)
     self.mcpu.assertLogContainsRegex("does not match its entry")
+    self.mcpu.assertLogContainsRegex(
+      "gnt-cluster renew-crypto --new-node-certificates")
 
   def testVerifyMasterCandidateUnregistered(self):
     client_cert = "client-cert-digest"
@@ -1285,6 +1296,8 @@
     op = opcodes.OpClusterVerifyGroup(group_name="default", verbose=True)
     self.ExecOpCode(op)
     self.mcpu.assertLogContainsRegex("does not have an entry")
+    self.mcpu.assertLogContainsRegex(
+      "gnt-cluster renew-crypto --new-node-certificates")
 
   def testVerifyMasterCandidateOtherNodesCert(self):
     client_cert = "client-cert-digest"
@@ -1297,6 +1310,8 @@
     op = opcodes.OpClusterVerifyGroup(group_name="default", verbose=True)
     self.ExecOpCode(op)
     self.mcpu.assertLogContainsRegex("using a certificate of another node")
+    self.mcpu.assertLogContainsRegex(
+      "gnt-cluster renew-crypto --new-node-certificates")
 
   def testNormalNodeStillInList(self):
     self._AddNormalNode()
@@ -1314,8 +1329,13 @@
         .Build()
     op = opcodes.OpClusterVerifyGroup(group_name="default", verbose=True)
     self.ExecOpCode(op)
-    self.mcpu.assertLogContainsRegex("not a master candidate")
-    self.mcpu.assertLogContainsRegex("still listed")
+    regexps = (
+      "not a master candidate",
+      "still listed",
+      "gnt-cluster renew-crypto --new-node-certificates",
+    )
+    for r in regexps:
+      self.mcpu.assertLogContainsRegex(r)
 
   def testNormalNodeStealingMasterCandidateCert(self):
     self._AddNormalNode()
@@ -1331,9 +1351,13 @@
         .Build()
     op = opcodes.OpClusterVerifyGroup(group_name="default", verbose=True)
     self.ExecOpCode(op)
-    self.mcpu.assertLogContainsRegex("not a master candidate")
-    self.mcpu.assertLogContainsRegex(
-      "certificate of another node which is master candidate")
+    regexps = (
+      "not a master candidate",
+      "certificate of another node which is master candidate",
+      "gnt-cluster renew-crypto --new-node-certificates",
+    )
+    for r in regexps:
+      self.mcpu.assertLogContainsRegex(r)
 
 
 class TestLUClusterVerifyGroupMethods(CmdlibTestCase):