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