On Fri, Apr 17, 2015 at 02:03:51PM +0000, Helga Velroyen wrote:
Hi!

On Fri, 17 Apr 2015 at 15:39 Petr Pudlak <[email protected]> wrote:

On Thu, Apr 16, 2015 at 05:31:56PM +0200, 'Helga Velroyen' via
ganeti-devel wrote:
>Testing the backend functions which update SSH keys is a
>pain and maintaining the tests even more. Therefore, this
>patch introduces a manager for all SSH key files of a
>clusters (ganeti_pub_keys and authorized_keys). It
>emulates all operations on these files for all nodes in
>the cluster.
>
>This has the following advantages:
>- One can query the state of the entire cluster in a
>  consisten way, for example "Do all nodes have this
>  master candidates' key?" instead of tediously evaluating
>  a history of mock calls.
>- The file manager emulates both local changes in the
>  master nodes' key files and changes on other nodes'
>  key files using the ssh_update tool. This way, the
>  state of the cluster ssh files is managed consistently
>  no matter by what mechanism they were changed.
>- The file manager offers a couple of convenience
>  functions to set up the test data and to query their
>  state after test operation.
>
>Note that this might look like a lot of code, but it
>vastly simplifies the current unit tests and it will
>make future tests (for example for invalid calls) much
>more easier. As it is a test utility, it is properly
>documented to make it maintainable.
>
>Signed-off-by: Helga Velroyen <[email protected]>
>---
> Makefile.am              |   1 +
> test/py/testutils_ssh.py | 491
+++++++++++++++++++++++++++++++++++++++++++++++
> 2 files changed, 492 insertions(+)
> create mode 100644 test/py/testutils_ssh.py
>
>diff --git a/Makefile.am b/Makefile.am
>index 7312ea1..518eb2b 100644
>--- a/Makefile.am
>+++ b/Makefile.am
>@@ -1842,6 +1842,7 @@ python_test_support = \
>       test/py/__init__.py \
>       test/py/lockperf.py \
>       test/py/testutils.py \
>+      test/py/testutils_ssh.py \
>       test/py/mocks.py \
>       test/py/cmdlib/__init__.py \
>       test/py/cmdlib/testsupport/__init__.py \
>diff --git a/test/py/testutils_ssh.py b/test/py/testutils_ssh.py
>new file mode 100644
>index 0000000..1e7edf8
>--- /dev/null
>+++ b/test/py/testutils_ssh.py
>@@ -0,0 +1,491 @@
>+#!/usr/bin/python
>+#
>+
>+# Copyright (C) 2010, 2013, 2015 Google Inc.
>+# All rights reserved.
>+#
>+# Redistribution and use in source and binary forms, with or without
>+# modification, are permitted provided that the following conditions are
>+# met:
>+#
>+# 1. Redistributions of source code must retain the above copyright
notice,
>+# this list of conditions and the following disclaimer.
>+#
>+# 2. Redistributions in binary form must reproduce the above copyright
>+# notice, this list of conditions and the following disclaimer in the
>+# documentation and/or other materials provided with the distribution.
>+#
>+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
>+# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
>+# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR
>+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
>+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
>+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
>+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
>+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
>+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
>+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
>+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
>+
>+
>+"""Helper class to test ssh-related code."""
>+
>+from ganeti import constants
>+from ganeti import pathutils
>+
>+
>+class FakeSshFileManager(object):
>+  """Class which 'fakes' the lowest layer of SSH key manipulation.
>+
>+  There are various operations which touch the nodes' SSH keys and their
>+  respective key files (authorized_keys and ganeti_pub_keys). Those are
>+  tedious to test as file operations have to be mocked on different
levels
>+  (direct access to the authorized_keys and ganeti_pub_keys) of the
master
>+  node, indirect access to those files of the non-master nodes (via the
>+  ssh_update tool). In order to make unit tests of those operations more
>+  readable and managable, we introduce this class, which mocks all
>+  direct and indirect access to SSH key files on all nodes. This way,
>+  the state of this FakeSshFileManager represents the state of a
cluster's
>+  nodes' SSH key files in a consise and easily accessible way.
>+
>+  """
>+  def __init__(self):
>+    # Dictionary mapping node name to node properties. The properties
>+    # are a tuple of (node_uuid, ssh_key, is_potential_master_candidate,
>+    # is_master_candidate, is_master).
>+    self._all_node_data = {}

Perhaps it'd be easier from a long-term maintenance perpsective to have a
dedicated class instead of 5-tuple). It'd be easier to extend, if needed,
field names would be more descriptive than just tuples, and some method
signatures might get simler. Or perhaps a class created by
`namedtuple(...)`
would be a good option. But I'd leave it as your call, what do you think
it's better, I'm not that Python expert.


Indeed, I had this thought as well. I wasn't sure if this would be
overkill, but I kinda like the idea. I'll put it on my todo list to change
it in a separate change (as this patch series is already long and merging
this change into every patch would be a pain).



>+    # Dictionary emulating the authorized keys files of all nodes. The
>+    # indices of the dictionary are the node names, the values are lists
>+    # of keys (strings).
>+    self._authorized_keys = {}

Is there a reason for using lists as values instead of sets? For example to
keep the order of the keys? Just it seems that using sets would simplify a
lot of code below (add if not present, remove etc.).


Actually no, this is a very good point. I'll update that.



>+    # Dictionary emulating the public keys file of all nodes. The indices
>+    # of the dictionary are the node names where the public key file is
>+    # 'located' (if it wasn't faked). The values of the dictionary are
>+    # dictionaries itself. Each of those dictionaries is indexed by the
>+    # node UUIDs mapping to a list of public keys.
>+    self._public_keys = {} # dict of dicts
>+    # Node name of the master node
>+    self._master_node_name = None
>+
>+  def _SetMasterNodeName(self):
>+    self._master_node_name = [name for name, (_, _, _, _, master)
>+                              in self._all_node_data.items() if
master][0]
>+
>+  def GetMasterNodeName(self):
>+    return self._master_node_name
>+
>+  def _CreateNodeDict(self, num_nodes, num_pot_mcs, num_mcs):
>+    """Creates a dictionary of all nodes and their properties."""
>+
>+    self._all_node_data = {}
>+    for i in range(num_nodes):
>+      name = "node_name_%i" % i
>+      uuid = "node_uuid_%i" % i
>+      key = "key%s" % i
>+      self._public_keys[name] = {}
>+      self._authorized_keys[name] = []
>+
>+      pot_mc = None
>+      if i in range(num_pot_mcs):
>+        pot_mc = True
>+      else:
>+        pot_mc = False

Could be replaced by

   pot_mc = i < num_pot_mcs


Ack.



>+
>+      mc = None
>+      if i in range(num_mcs):
>+        mc = True
>+      else:
>+        mc = False

.. also here.


Ack.



>+
>+      master = None
>+      if i == num_mcs / 2:
>+        master = True
>+      else:
>+        master = False

also here - master = i == num_mcs / 2


Ack.



>+
>+      self._all_node_data[name] = (uuid, key, pot_mc, mc, master)
>+
>+  def _FillPublicKeyOfOneNode(self, receiving_node_name):
>+    _, _, is_pot_mc, _, _ = self._all_node_data[receiving_node_name]
>+    # Nodes which are not potential master candidates receive no keys
>+    if not is_pot_mc:
>+      return
>+    for uuid, key, pot_mc, _, _ in self._all_node_data.values():
>+      if pot_mc:
>+        self._public_keys[receiving_node_name][uuid] = key
>+
>+  def _FillAuthorizedKeyOfOneNode(self, receiving_node_name):
>+    for _, key, _, mc, _ in self._all_node_data.values():
>+      if mc:
>+        self._authorized_keys[receiving_node_name].append(key)
>+
>+  def InitAllNodes(self, num_nodes, num_pot_mcs, num_mcs):
>+    """Initializes the entire state of the cluster wrt SSH keys.
>+
>+    @type num_nodes: int
>+    @param num_nodes: number of nodes in the cluster
>+    @type num_pot_mcs: int
>+    @param num_pot_mcs: number of potential master candidates in the
cluster
>+    @type num_mcs: in
>+    @param num_mcs: number of master candidates in the cluster.
>+
>+    """
>+    self._public_keys = {}
>+    self._authorized_keys = {}
>+    self._CreateNodeDict(num_nodes, num_pot_mcs, num_mcs)
>+    for node in self._all_node_data.keys():
>+      self._FillPublicKeyOfOneNode(node)
>+      self._FillAuthorizedKeyOfOneNode(node)
>+    self._SetMasterNodeName()
>+
>+  def GetSshPortMap(self, port):
>+    """Creates a SSH port map with all nodes mapped to the given port.
>+
>+    @type port: int
>+    @param port: SSH port number for all nodes
>+
>+    """
>+    port_map = {}
>+    for node in self._all_node_data.keys():
>+      port_map[node] = port
>+    return port_map
>+
>+  def GetAllNodeNames(self):
>+    return self._all_node_data.keys()
>+
>+  def GetAllPotentialMasterCandidateNodeNames(self):
>+    return [name for name, (_, _, pot_mc, _, _)
>+            in self._all_node_data.items() if pot_mc]
>+
>+  def GetAllMasterCandidateUuids(self):
>+    return [uuid for uuid, _, _, mc, _
>+            in self._all_node_data.values() if mc]
>+
>+  def GetAllPotentialMasterCandidates(self):
>+    return [(name, uuid, key, pot_mc, mc, master)
>+            for name, (uuid, key, pot_mc, mc, master)
>+            in self._all_node_data.items() if pot_mc]
>+
>+  def SetOrAddNode(self, name, uuid, key, pot_mc, mc, master):
>+    """Adds a new node to the state of the file manager.
>+
>+    This is necessary when testing to add new nodes to the cluster.
Otherwise
>+    this new node's state would not be evaluated properly with the
assertion
>+    fuctions.
>+
>+    @type name: string
>+    @param name: name of the new node
>+    @type uuid: string
>+    @param uuid: UUID of the new node
>+    @type key: string
>+    @param key: SSH key of the new node
>+    @type pot_mc: boolean
>+    @param pot_mc: whether the new node is a potential master candidate
>+    @type mc: boolean
>+    @param mc: whether the new node is a master candidate
>+    @type master: boolean
>+    @param master: whether the new node is the master
>+
>+    """
>+    self._all_node_data[name] = (uuid, key, pot_mc, mc, master)
>+    if name not in self._authorized_keys:
>+      self._authorized_keys[name] = []
>+    if mc:
>+      if not key in self._authorized_keys[name]:
>+        self._authorized_keys[name].append(key)
>+    if name not in self._public_keys:
>+      self._public_keys[name] = {}
>+
>+  def NodeHasPublicKey(self, file_node_name, key_node_uuid, key):
>+    """Checks whether a node has another node's public key.
>+
>+    @type file_node_name: string
>+    @param file_node_name: name of the node whose public key file is
inspected
>+    @type key_node_uuid: string
>+    @param key_node_uuid: UUID of the node whose key is checked for
>+    @rtype: boolean
>+    @return: True if the key_node's UUID is found with the machting key
'key'
>+
>+    """
>+    for (node_uuid, pub_keys) in
self._public_keys[file_node_name].items():
>+      if key in pub_keys and key_node_uuid == node_uuid:
>+        return True
>+    return False
>+
>+  def NodeHasAuthorizedKey(self, file_node_name, key):
>+    """Checks whether a node has a particular key in its authorized_keys
file.
>+
>+    @type file_node_name: string
>+    @param file_node_name: name of the node whose authorized_key file is
>+      inspected
>+    @type key: string
>+    @param key: key which is expected to be found in the node's
authorized_key
>+      file
>+    @rtype: boolean
>+    @return: True if the key is found in the node's authorized_key file
>+
>+    """
>+    return key in self._authorized_keys[file_node_name]
>+
>+  def AllNodesHaveAuthorizedKey(self, key):
>+    """Check if all nodes have a particular key in their auth. keys file.
>+
>+    @type key: string
>+    @param key: key exptected to be present in all node's
authorized_keys file
>+    @rtype: boolean
>+    @return: True if key is present in all node's authorized_keys file
>+
>+    """
>+    for name in self._all_node_data.keys():
>+      node_auth_keys = self._authorized_keys[name]
>+      if key not in node_auth_keys:

Just nitpicking, probably not necessary to assign the variable, could be
directly in `if` (also applies to other methods below), but I'm not sure
what's more idiomatic Python.


Ack.



>+        raise Exception("Node '%s' does not have the key '%s' in its"
>+                        " 'authorized_keys' file." % (name, key))
>+    return True

I'm a bit puzzled by the output of the method. It'd expect it to either
return True/False (like in the following method), or have no return value
and throw the exception if a key is missing. Currently it returns true or
throws the exception, so the return value has no use.


I agree. I realized that already and have (separate) patch already prepared
for this. As it would be a pain to merge that into every patch of the
series, I'd rather simplify that properly at the end. Would that be fine
for you?



>+
>+  def NoNodeHasAuthorizedKey(self, key):
>+    """Check if none of the nodes has a particular key in their auth.
keys file.
>+
>+    @type key: string
>+    @param key: key exptected to be present in all node's
authorized_keys file
>+    @rtype: boolean
>+    @return: True if key is not present in all node's authorized_keys
file
>+
>+    """
>+    for name in self._all_node_data.keys():
>+      node_auth_keys = self._authorized_keys[name]
>+      if key in node_auth_keys:
>+        return False
>+    return True
>+
>+  def NoNodeHasPublicKey(self, uuid, key):
>+    """Check if none of the nodes have the given public key in their
file.
>+
>+    @type uuid: string
>+    @param uuid: UUID of the node whose key is looked for
>+    @type key: string
>+    @param key: SSH key to be looked for
>+
>+    """
>+    for name in self._all_node_data.keys():
>+      node_pub_keys = self._public_keys[name]
>+      if (uuid, key) in node_pub_keys.items():
>+        return False
>+    return True
>+
>+  def PotentialMasterCandidatesOnlyHavePublicKey(self, query_node_name):
>+    """Checks if the node's key is on all potential master candidates
only.
>+
>+    This ensures that the node's key is in all public key files of all
>+    potential master candidates, and it also checks whether the key is
>+    *not* in all other nodes's key files.
>+
>+    @param query_node_name: name of the node whose key is expected to be
>+                            in the public key file of all potential
master
>+                            candidates
>+    @type query_node_name: string
>+
>+    """
>+    query_node_uuid, query_node_key, _, _, _ = \
>+        self._all_node_data[query_node_name]
>+    for name, (_, _, pot_mc, _, _) in self._all_node_data.items():
>+      has_key = self.NodeHasPublicKey(name, query_node_uuid,
query_node_key)
>+      if pot_mc:
>+        if not has_key:
>+          raise Exception("Potential master candidate '%s' does not have
the"
>+                          " key.")
>+      else:
>+        if has_key:
>+          raise Exception("Normal node (not potential master candidate)
'%s'"
>+                          " does have the key, although it should not
have.")
>+
>+  # Disabling a pylint warning about unused parameters. Those need
>+  # to be here to properly mock the real methods.
>+  # pylint: disable=W0613
>+  def RunCommand(self, cluster_name, node, base_cmd, port, data,
>+                 debug=False, verbose=False, use_cluster_key=False,
>+                 ask_key=False, strict_host_check=False,
>+                 ensure_version=False):
>+    """This emulates ssh.RunSshCmdWithStdin calling ssh_update.
>+
>+    While in real SSH operations, ssh.RunSshCmdWithStdin is called
>+    with the command ssh_update to manipulate a remote node's SSH
>+    key files (authorized_keys and ganeti_pub_key) file, this method
>+    emulates the operation by manipulating only its internal dictionaries
>+    of SSH keys. No actual key files of any node is touched.
>+
>+    """
>+    assert base_cmd == pathutils.SSH_UPDATE
>+
>+    if constants.SSHS_SSH_AUTHORIZED_KEYS in data:
>+      instructions_auth = data[constants.SSHS_SSH_AUTHORIZED_KEYS]
>+      self._HandleAuthorizedKeys(instructions_auth, node)
>+    if constants.SSHS_SSH_PUBLIC_KEYS in data:
>+      instructions_pub = data[constants.SSHS_SSH_PUBLIC_KEYS]
>+      self._HandlePublicKeys(instructions_pub, node)
>+  # pylint: enable=W0613
>+
>+  def _EnsureAuthKeyFile(self, file_node_name):
>+    if file_node_name not in self._authorized_keys:
>+      self._authorized_keys[file_node_name] = []
>+
>+  def _AddAuthorizedKey(self, file_node_name, ssh_key):
>+    self._EnsureAuthKeyFile(file_node_name)
>+    if not ssh_key in self._authorized_keys[file_node_name]:
>+      self._authorized_keys[file_node_name].append(ssh_key)
>+
>+  def _RemoveAuthorizedKey(self, file_node_name, key):
>+    self._EnsureAuthKeyFile(file_node_name)
>+    self._authorized_keys[file_node_name][:] = \
>+        [k for k in self._authorized_keys[file_node_name] if k != key]
>+
>+  def _HandleAuthorizedKeys(self, instructions, node):
>+    (action, authorized_keys) = instructions
>+    ssh_keys = []
>+    for ssh_key in authorized_keys.values():
>+      ssh_keys.append(ssh_key)

Could this be replaced by just

   ssh_keys = authorized_keys.values()

?


Indeed, I think that was some refactoring mishap.



>+    if action == constants.SSHS_ADD:
>+      for ssh_key in ssh_keys:
>+        self._AddAuthorizedKey(node, ssh_key)
>+    elif action == constants.SSHS_REMOVE:
>+      for ssh_key in ssh_keys:
>+        self._RemoveAuthorizedKey(node, ssh_key)
>+    else:
>+      raise Exception("Unsupported action: %s" % action)
>+
>+  def _EnsurePublicKeyFile(self, file_node_name):
>+    if file_node_name not in self._public_keys:
>+      self._public_keys[file_node_name] = {}
>+
>+  def _ClearPublicKeys(self, file_node_name):
>+    self._public_keys[file_node_name] = {}
>+
>+  def _OverridePublicKeys(self, ssh_keys, file_node_name):
>+    self._ClearPublicKeys(file_node_name)
>+    for key_node_uuid, node_keys in ssh_keys.items():
>+      if key_node_uuid in self._public_keys[file_node_name]:
>+        raise Exception("Duplicate node in ssh_update data.")
>+      self._public_keys[file_node_name][key_node_uuid] = node_keys
>+
>+  def _AddPublicKeys(self, public_keys, file_node_name):
>+    self._EnsurePublicKeyFile(file_node_name)
>+    for key_node_uuid, keys in public_keys.items():
>+      self._public_keys[file_node_name][key_node_uuid] = keys
>+
>+  def _ReplaceOrAddPublicKeys(self, public_keys, file_node_name):
>+    self._EnsurePublicKeyFile(file_node_name)
>+    for key_node_uuid, keys in public_keys.items():
>+      self._public_keys[file_node_name][key_node_uuid] = keys

These two methods seem to me to be the same, perhaps some check is missing
in the first one?


I removed the first one alltogether and just called the second one. This is
consistent the implementation that it emulates.



>+
>+  def _RemovePublicKeys(self, public_keys, file_node_name):
>+    self._EnsurePublicKeyFile(file_node_name)
>+    for key_node_uuid, _ in public_keys.items():
>+      if key_node_uuid in self._public_keys[file_node_name]:
>+        self._public_keys[file_node_name][key_node_uuid] = []
>+
>+  def _HandlePublicKeys(self, instructions, node):
>+    (action, public_keys) = instructions
>+    if action == constants.SSHS_OVERRIDE:
>+      self._OverridePublicKeys(public_keys, node)
>+    elif action == constants.SSHS_ADD:
>+      self._AddPublicKeys(public_keys, node)
>+    elif action == constants.SSHS_REPLACE_OR_ADD:
>+      self._ReplaceOrAddPublicKeys(public_keys, node)
>+    elif action == constants.SSHS_REMOVE:
>+      self._RemovePublicKeys(public_keys, node)
>+    elif action == constants.SSHS_CLEAR:
>+      self._ClearPublicKeys(node)
>+    else:
>+      raise Exception("Unsupported action: %s." % action)
>+
>+  # pylint: disable=W0613
>+  def AddAuthorizedKeys(self, file_obj, keys):
>+    """Emulates ssh.AddAuthorizedKeys on the master node.
>+
>+    Instead of actually mainpulating the authorized_keys file, this
method
>+    keeps the state of the file in a dictionary in memory.
>+
>+    @see: C{ssh.AddAuthorizedKeys}
>+
>+    """
>+    assert self._master_node_name
>+    for key in keys:
>+      self._AddAuthorizedKey(self._master_node_name, key)
>+
>+  def RemoveAuthorizedKeys(self, file_name, keys):
>+    """Emulates ssh.RemoveAuthorizeKeys on the master node.
>+
>+    Instead of actually mainpulating the authorized_keys file, this
method
>+    keeps the state of the file in a dictionary in memory.
>+
>+    @see: C{ssh.RemoveAuthorizedKeys}
>+
>+    """
>+    assert self._master_node_name
>+    for key in keys:
>+      self._RemoveAuthorizedKey(self._master_node_name, key)
>+
>+  def AddPublicKey(self, new_uuid, new_key, **kwargs):
>+    """Emulates ssh.AddPublicKey on the master node.
>+
>+    Instead of actually mainpulating the authorized_keys file, this
method
>+    keeps the state of the file in a dictionary in memory.
>+
>+    @see: C{ssh.AddPublicKey}
>+
>+    """
>+    assert self._master_node_name
>+    key_dict = {new_uuid: new_key}
>+    self._AddPublicKeys(key_dict, self._master_node_name)
>+
>+  def RemovePublicKey(self, target_uuid, **kwargs):
>+    """Emulates ssh.RemovePublicKey on the master node.
>+
>+    Instead of actually mainpulating the authorized_keys file, this
method
>+    keeps the state of the file in a dictionary in memory.
>+
>+    @see: {ssh.RemovePublicKey}
>+
>+    """
>+    assert self._master_node_name
>+    key_dict = {target_uuid: []}
>+    self._RemovePublicKeys(key_dict, self._master_node_name)
>+
>+  def QueryPubKeyFile(self, target_uuids, **kwargs):
>+    """Emulates ssh.QueryPubKeyFile on the master node.
>+
>+    Instead of actually mainpulating the authorized_keys file, this
method
>+    keeps the state of the file in a dictionary in memory.
>+
>+    @see: C{ssh.QueryPubKey}
>+
>+    """
>+    assert self._master_node_name
>+    all_keys = target_uuids is None
>+    if all_keys:
>+      return self._public_keys[self._master_node_name]
>+
>+    if isinstance(target_uuids, str):
>+      target_uuids = [target_uuids]
>+    result_dict = {}
>+    for key_node_uuid, key in \
>+        self._public_keys[self._master_node_name].items():
>+      if key_node_uuid in target_uuids:
>+        result_dict[key_node_uuid] = key
>+    return result_dict
>+
>+  def ReplaceNameByUuid(self, node_uuid, node_name, **kwargs):
>+    """Emulates ssh.ReplaceNameByUuid on the master node.
>+
>+    Instead of actually mainpulating the authorized_keys file, this
method
>+    keeps the state of the file in a dictionary in memory.
>+
>+    @see: C{ssh.ReplacenameByUuid}
>+
>+    """
>+    assert self._master_node_name
>+    if node_name in self._public_keys[self._master_node_name]:
>+      self._public_keys[self._master_node_name][node_uuid] = \
>+        self._public_keys[self._master_node_name][node_name][:]
>+      del self._public_keys[self._master_node_name][node_name]
>+  # pylint: enable=W0613
>--
>2.2.0.rc0.207.ga3a616c
>

Rest LGTM


Interdiff:

diff --git a/test/py/testutils_ssh.py b/test/py/testutils_ssh.py
index 1e7edf8..bc704b0 100644
--- a/test/py/testutils_ssh.py
+++ b/test/py/testutils_ssh.py
@@ -55,7 +55,7 @@ class FakeSshFileManager(object):
    # is_master_candidate, is_master).
    self._all_node_data = {}
    # Dictionary emulating the authorized keys files of all nodes. The
-    # indices of the dictionary are the node names, the values are lists
+    # indices of the dictionary are the node names, the values are sets
    # of keys (strings).
    self._authorized_keys = {}
    # Dictionary emulating the public keys file of all nodes. The indices
@@ -83,25 +83,11 @@ class FakeSshFileManager(object):
      uuid = "node_uuid_%i" % i
      key = "key%s" % i
      self._public_keys[name] = {}
-      self._authorized_keys[name] = []
+      self._authorized_keys[name] = set()

-      pot_mc = None
-      if i in range(num_pot_mcs):
-        pot_mc = True
-      else:
-        pot_mc = False
-
-      mc = None
-      if i in range(num_mcs):
-        mc = True
-      else:
-        mc = False
-
-      master = None
-      if i == num_mcs / 2:
-        master = True
-      else:
-        master = False
+      pot_mc = i < num_pot_mcs
+      mc = i < num_mcs
+      master = i == num_mcs / 2

      self._all_node_data[name] = (uuid, key, pot_mc, mc, master)

@@ -117,7 +103,7 @@ class FakeSshFileManager(object):
  def _FillAuthorizedKeyOfOneNode(self, receiving_node_name):
    for _, key, _, mc, _ in self._all_node_data.values():
      if mc:
-        self._authorized_keys[receiving_node_name].append(key)
+        self._authorized_keys[receiving_node_name].add(key)

  def InitAllNodes(self, num_nodes, num_pot_mcs, num_mcs):
    """Initializes the entire state of the cluster wrt SSH keys.
@@ -189,10 +175,9 @@ class FakeSshFileManager(object):
    """
    self._all_node_data[name] = (uuid, key, pot_mc, mc, master)
    if name not in self._authorized_keys:
-      self._authorized_keys[name] = []
+      self._authorized_keys[name] = set()
    if mc:
-      if not key in self._authorized_keys[name]:
-        self._authorized_keys[name].append(key)
+      self._authorized_keys[name].add(key)
    if name not in self._public_keys:
      self._public_keys[name] = {}

@@ -237,8 +222,7 @@ class FakeSshFileManager(object):

    """
    for name in self._all_node_data.keys():
-      node_auth_keys = self._authorized_keys[name]
-      if key not in node_auth_keys:
+      if key not in self._authorized_keys[name]:
        raise Exception("Node '%s' does not have the key '%s' in its"
                        " 'authorized_keys' file." % (name, key))
    return True
@@ -327,23 +311,20 @@ class FakeSshFileManager(object):

  def _EnsureAuthKeyFile(self, file_node_name):
    if file_node_name not in self._authorized_keys:
-      self._authorized_keys[file_node_name] = []
+      self._authorized_keys[file_node_name] = set()

  def _AddAuthorizedKey(self, file_node_name, ssh_key):
    self._EnsureAuthKeyFile(file_node_name)
-    if not ssh_key in self._authorized_keys[file_node_name]:
-      self._authorized_keys[file_node_name].append(ssh_key)
+    self._authorized_keys[file_node_name].add(ssh_key)

  def _RemoveAuthorizedKey(self, file_node_name, key):
    self._EnsureAuthKeyFile(file_node_name)
-    self._authorized_keys[file_node_name][:] = \
-        [k for k in self._authorized_keys[file_node_name] if k != key]
+    self._authorized_keys[file_node_name] = \
+        set([k for k in self._authorized_keys[file_node_name] if k != key])

  def _HandleAuthorizedKeys(self, instructions, node):
    (action, authorized_keys) = instructions
-    ssh_keys = []
-    for ssh_key in authorized_keys.values():
-      ssh_keys.append(ssh_key)
+    ssh_keys = authorized_keys.values()
    if action == constants.SSHS_ADD:
      for ssh_key in ssh_keys:
        self._AddAuthorizedKey(node, ssh_key)
@@ -367,11 +348,6 @@ class FakeSshFileManager(object):
        raise Exception("Duplicate node in ssh_update data.")
      self._public_keys[file_node_name][key_node_uuid] = node_keys

-  def _AddPublicKeys(self, public_keys, file_node_name):
-    self._EnsurePublicKeyFile(file_node_name)
-    for key_node_uuid, keys in public_keys.items():
-      self._public_keys[file_node_name][key_node_uuid] = keys
-
  def _ReplaceOrAddPublicKeys(self, public_keys, file_node_name):
    self._EnsurePublicKeyFile(file_node_name)
    for key_node_uuid, keys in public_keys.items():
@@ -388,7 +364,7 @@ class FakeSshFileManager(object):
    if action == constants.SSHS_OVERRIDE:
      self._OverridePublicKeys(public_keys, node)
    elif action == constants.SSHS_ADD:
-      self._AddPublicKeys(public_keys, node)
+      self._ReplaceOrAddPublicKeys(public_keys, node)
    elif action == constants.SSHS_REPLACE_OR_ADD:
      self._ReplaceOrAddPublicKeys(public_keys, node)
    elif action == constants.SSHS_REMOVE:

LGTM, thanks

Reply via email to