| #!/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 |
| from ganeti import errors |
| |
| from collections import namedtuple |
| |
| |
| 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 named tuple of (node_uuid, ssh_key, is_potential_master_candidate, |
| # 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 sets |
| # of keys (strings). |
| self._authorized_keys = {} |
| # 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 |
| # Dictionary mapping nodes by name to number of retries where 'RunCommand' |
| # succeeds. For example if set to '3', RunCommand will fail two times when |
| # called for this node before it succeeds in the 3rd retry. |
| self._max_retries = {} |
| # Dictionary mapping nodes by name to number of retries which |
| # 'RunCommand' has already carried out. |
| self._retries = {} |
| |
| self._AssertTypePublicKeys() |
| self._AssertTypeAuthorizedKeys() |
| |
| _NodeInfo = namedtuple( |
| "NodeInfo", |
| ["uuid", |
| "key", |
| "is_potential_master_candidate", |
| "is_master_candidate", |
| "is_master"]) |
| |
| def _SetMasterNodeName(self): |
| self._master_node_name = [name for name, node_info |
| in self._all_node_data.items() |
| if node_info.is_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] = set() |
| |
| pot_mc = i < num_pot_mcs |
| mc = i < num_mcs |
| master = i == num_mcs / 2 |
| |
| self._all_node_data[name] = self._NodeInfo(uuid, key, pot_mc, mc, master) |
| |
| self._AssertTypePublicKeys() |
| self._AssertTypeAuthorizedKeys() |
| |
| def _FillPublicKeyOfOneNode(self, receiving_node_name): |
| node_info = self._all_node_data[receiving_node_name] |
| # Nodes which are not potential master candidates receive no keys |
| if not node_info.is_potential_master_candidate: |
| return |
| for node_info in self._all_node_data.values(): |
| if node_info.is_potential_master_candidate: |
| self._public_keys[receiving_node_name][node_info.uuid] = [node_info.key] |
| |
| def _FillAuthorizedKeyOfOneNode(self, receiving_node_name): |
| for node_name, node_info in self._all_node_data.items(): |
| if node_info.is_master_candidate \ |
| or node_name == receiving_node_name: |
| self._authorized_keys[receiving_node_name].add(node_info.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() |
| self._AssertTypePublicKeys() |
| self._AssertTypeAuthorizedKeys() |
| |
| def SetMaxRetries(self, node_name, retries): |
| """Set the number of unsuccessful retries of 'RunCommand' per node. |
| |
| @type node_name: string |
| @param node_name: name of the node |
| @type retries: integer |
| @param retries: number of unsuccessful retries |
| |
| """ |
| self._max_retries[node_name] = retries |
| |
| 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): |
| """Returns all node names of the cluster. |
| |
| @rtype: list of str |
| @returns: list of all node names |
| """ |
| return self._all_node_data.keys() |
| |
| def GetAllNodeUuids(self): |
| """Returns all node UUIDs of the cluster. |
| |
| @rtype: list of str |
| @returns: list of all node UUIDs |
| """ |
| return [node.uuid for node in self._all_node_data.values()] |
| |
| def GetAllPotentialMasterCandidateNodeNames(self): |
| return [name for name, node_info |
| in self._all_node_data.items() |
| if node_info.is_potential_master_candidate] |
| |
| def GetAllMasterCandidateUuids(self): |
| return [node_info.uuid for node_info |
| in self._all_node_data.values() if node_info.is_master_candidate] |
| |
| def GetAllPurePotentialMasterCandidates(self): |
| """Get the potential master candidates which are not master candidates. |
| |
| @rtype: list of tuples (string, C{_NodeInfo}) |
| @returns: list of tuples of node name and node information of nodes |
| which are potential master candidates but not master |
| candidates |
| """ |
| return [(name, node_info) for name, node_info |
| in self._all_node_data.items() |
| if node_info.is_potential_master_candidate and |
| not node_info.is_master_candidate] |
| |
| def GetAllMasterCandidates(self): |
| """Get all master candidate nodes. |
| |
| @rtype: list of tuples (string, C{_NodeInfo}) |
| @returns: list of tuples of node name and node information of master |
| candidate nodes. |
| """ |
| return [(name, node_info) for name, node_info |
| in self._all_node_data.items() if node_info.is_master_candidate] |
| |
| def GetAllNormalNodes(self): |
| """Get all normal nodes. |
| |
| Normal nodes are nodes that are neither master, master candidate nor |
| potential master candidate. |
| |
| @rtype: list of tuples (string, C{_NodeInfo}) |
| @returns: list of tuples of node name and node information of normal |
| nodes |
| """ |
| return [(name, node_info) for name, node_info |
| in self._all_node_data.items() if not node_info.is_master_candidate |
| and not node_info.is_potential_master_candidate] |
| |
| def GetAllNodesDiverse(self): |
| """This returns all nodes in a diverse order. |
| |
| This will return all nodes, but makes sure that they are ordered so that |
| the list will contain in a round-robin fashion, a master candidate, |
| a potential master candidate, a normal node, then again a master |
| candidate, etc. |
| |
| @rtype: list of tuples (string, C{_NodeInfo}) |
| @returns: list of tuples of node name and node information |
| |
| """ |
| master_candidates = self.GetAllMasterCandidates() |
| potential_master_candidates = self.GetAllPurePotentialMasterCandidates() |
| normal_nodes = self.GetAllNormalNodes() |
| |
| mixed_list = [] |
| |
| i = 0 |
| |
| assert (len(self._all_node_data) == len(master_candidates) |
| + len(potential_master_candidates) + len(normal_nodes)) |
| |
| while len(mixed_list) < len(self._all_node_data): |
| if i % 3 == 0: |
| if master_candidates: |
| mixed_list.append(master_candidates[0]) |
| master_candidates = master_candidates[1:] |
| elif i % 3 == 1: |
| if potential_master_candidates: |
| mixed_list.append(potential_master_candidates[0]) |
| potential_master_candidates = potential_master_candidates[1:] |
| else: # i % 3 == 2 |
| if normal_nodes: |
| mixed_list.append(normal_nodes[0]) |
| normal_nodes = normal_nodes[1:] |
| i += 1 |
| |
| return mixed_list |
| |
| def GetPublicKeysOfNode(self, node): |
| """Returns the public keys that are stored on the given node. |
| |
| @rtype: dict of str to list of str |
| @returns: a mapping of node names to a list of public keys |
| |
| """ |
| return self._public_keys[node] |
| |
| def GetAuthorizedKeysOfNode(self, node): |
| """Returns the authorized keys of the given node. |
| |
| @type node: string |
| @param node: name of the node |
| @rtype: list of str |
| @returns: a list of authorized keys that are stored on that node |
| |
| """ |
| return self._authorized_keys[node] |
| |
| def GetKeyOfNode(self, node): |
| """Returns the SSH key of the given node. |
| |
| @type node: string |
| @param node: name of the node |
| @rtype: string |
| @returns: the SSH key of the node |
| |
| """ |
| return self._all_node_data[node].key |
| |
| 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 |
| functions. |
| |
| @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] = self._NodeInfo(uuid, key, pot_mc, mc, master) |
| if name not in self._authorized_keys: |
| self._authorized_keys[name] = set() |
| if mc: |
| self._authorized_keys[name].add(key) |
| if name not in self._public_keys: |
| self._public_keys[name] = {} |
| self._AssertTypePublicKeys() |
| self._AssertTypeAuthorizedKeys() |
| |
| 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 AssertNodeSetOnlyHasAuthorizedKey(self, node_set, query_node_key): |
| """Check if nodes in the given set only have a particular authorized key. |
| |
| @type node_set: list of strings |
| @param node_set: list of nodes who are supposed to have the key |
| @type query_node_key: string |
| @param query_node_key: key which is looked for |
| |
| """ |
| assert isinstance(node_set, list) |
| for node_name in self._all_node_data.keys(): |
| if node_name in node_set: |
| if not self.NodeHasAuthorizedKey(node_name, query_node_key): |
| raise Exception("Node '%s' does not have authorized key '%s'." |
| % (node_name, query_node_key)) |
| else: |
| if self.NodeHasAuthorizedKey(node_name, query_node_key): |
| raise Exception("Node '%s' has authorized key '%s' although it" |
| " should not." % (node_name, query_node_key)) |
| |
| def AssertAllNodesHaveAuthorizedKey(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 |
| @raise Exception: if a node does not have the authorized key. |
| |
| """ |
| self.AssertNodeSetOnlyHasAuthorizedKey(self._all_node_data.keys(), key) |
| |
| def AssertNoNodeHasAuthorizedKey(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 |
| @raise Exception: if a node *does* have the authorized key. |
| |
| """ |
| self.AssertNodeSetOnlyHasAuthorizedKey([], key) |
| |
| def AssertNodeSetOnlyHasPublicKey(self, node_set, query_node_uuid, |
| query_node_key): |
| """Check if nodes in the given set only have a particular public key. |
| |
| @type node_set: list of strings |
| @param node_set: list of nodes who are supposed to have the key |
| @type query_node_uuid: string |
| @param query_node_uuid: uuid of the node whose key is looked for |
| @type query_node_key: string |
| @param query_node_key: key which is looked for |
| |
| """ |
| for node_name in self._all_node_data.keys(): |
| if node_name in node_set: |
| if not self.NodeHasPublicKey(node_name, query_node_uuid, |
| query_node_key): |
| raise Exception("Node '%s' does not have public key '%s' of node" |
| " '%s'." % (node_name, query_node_key, |
| query_node_uuid)) |
| else: |
| if self.NodeHasPublicKey(node_name, query_node_uuid, query_node_key): |
| raise Exception("Node '%s' has public key '%s' of node" |
| " '%s' although it should not." |
| % (node_name, query_node_key, query_node_uuid)) |
| |
| def AssertNoNodeHasPublicKey(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 |
| @raise Exception: if a node *does* have the public key. |
| |
| """ |
| self.AssertNodeSetOnlyHasPublicKey([], uuid, key) |
| |
| def AssertPotentialMasterCandidatesOnlyHavePublicKey(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 |
| @raise Exception: when a potential master candidate does not have |
| the public key or a normal node *does* have a public key. |
| |
| """ |
| query_node_uuid, query_node_key, _, _, _ = \ |
| self._all_node_data[query_node_name] |
| potential_master_candidates = self.GetAllPotentialMasterCandidateNodeNames() |
| self.AssertNodeSetOnlyHasPublicKey( |
| potential_master_candidates, query_node_uuid, query_node_key) |
| |
| def _AssertTypePublicKeys(self): |
| """Asserts that the public key dictionary has the right types. |
| |
| This is helpful as an invariant that shall not be violated during the |
| tests due to type errors. |
| |
| """ |
| assert isinstance(self._public_keys, dict) |
| for node_file, pub_keys in self._public_keys.items(): |
| assert isinstance(node_file, str) |
| assert isinstance(pub_keys, dict) |
| for node_key, keys in pub_keys.items(): |
| assert isinstance(node_key, str) |
| assert isinstance(keys, list) |
| for key in keys: |
| assert isinstance(key, str) |
| |
| def _AssertTypeAuthorizedKeys(self): |
| """Asserts that the authorized keys dictionary has the right types. |
| |
| This is useful to check as an invariant that is not supposed to be violated |
| during the tests. |
| |
| """ |
| assert isinstance(self._authorized_keys, dict) |
| for node_file, auth_keys in self._authorized_keys.items(): |
| assert isinstance(node_file, str) |
| assert isinstance(auth_keys, set) |
| for key in auth_keys: |
| assert isinstance(key, str) |
| |
| # 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. |
| |
| """ |
| if node in self._max_retries: |
| if node not in self._retries: |
| self._retries[node] = 0 |
| self._retries[node] += 1 |
| if self._retries[node] < self._max_retries[node]: |
| raise errors.OpExecError("(Fake) SSH connection to node '%s' failed." |
| % node) |
| |
| 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) |
| if constants.SSHS_GENERATE in data: |
| instructions_generate = data[constants.SSHS_GENERATE] |
| self._GenerateNewKey(instructions_generate, node) |
| # pylint: enable=W0613 |
| |
| def _GenerateNewKey(self, instructions_generate, node): |
| """Generates a new key for the given node. |
| |
| Note that this is a very rudimentary generation of a new key. The key is |
| always generated with the same pattern, starting with 'new_key'. That |
| means if you run it twice, it will actually produce the same key. However, |
| for what we want to test, this is sufficient. |
| The 'suffix' instruction is also ignored and the key is directly overriden. |
| This works so far, but simplifies the tests a bit. It might be extended |
| in case it becomes necessary. |
| |
| @type instructions_generate: tuple of (string, integer, string) |
| @param instructions_generate: an instructions tuple for generating a new |
| SSH key. This has to comply to the C{_DATA_CHECK} description in |
| C{ssh_update.py}. |
| @type node: string |
| @param node: name of node |
| """ |
| (key_type, key_bits, suffix) = instructions_generate |
| assert key_type in constants.SSHK_ALL |
| assert key_bits > 0 |
| assert isinstance(suffix, str) |
| |
| new_key = "new_key_%s" % node |
| old_node_data = self._all_node_data[node] |
| |
| new_node_data = self._NodeInfo( |
| uuid=old_node_data.uuid, |
| key=new_key, |
| is_potential_master_candidate=old_node_data |
| .is_potential_master_candidate, |
| is_master_candidate=old_node_data.is_master_candidate, |
| is_master=old_node_data.is_master) |
| |
| self._all_node_data[node] = new_node_data |
| |
| def _EnsureAuthKeyFile(self, file_node_name): |
| if file_node_name not in self._authorized_keys: |
| self._authorized_keys[file_node_name] = set() |
| self._AssertTypePublicKeys() |
| self._AssertTypeAuthorizedKeys() |
| |
| def _AddAuthorizedKeys(self, file_node_name, ssh_keys): |
| """Mocks adding the given keys to the authorized_keys file.""" |
| assert isinstance(ssh_keys, list) |
| self._EnsureAuthKeyFile(file_node_name) |
| for key in ssh_keys: |
| self._authorized_keys[file_node_name].add(key) |
| self._AssertTypePublicKeys() |
| self._AssertTypeAuthorizedKeys() |
| |
| def _RemoveAuthorizedKeys(self, file_node_name, keys): |
| """Mocks removing the keys from authorized_keys on the given node. |
| |
| @param keys: list of ssh keys |
| @type keys: list of strings |
| |
| """ |
| self._EnsureAuthKeyFile(file_node_name) |
| self._authorized_keys[file_node_name] = \ |
| set([k for k in self._authorized_keys[file_node_name] if k not in keys]) |
| self._AssertTypeAuthorizedKeys() |
| |
| def _HandleAuthorizedKeys(self, instructions, node): |
| (action, authorized_keys) = instructions |
| ssh_key_sets = authorized_keys.values() |
| if action == constants.SSHS_ADD: |
| for ssh_keys in ssh_key_sets: |
| self._AddAuthorizedKeys(node, ssh_keys) |
| elif action == constants.SSHS_REMOVE: |
| for ssh_keys in ssh_key_sets: |
| self._RemoveAuthorizedKeys(node, ssh_keys) |
| else: |
| raise Exception("Unsupported action: %s" % action) |
| self._AssertTypeAuthorizedKeys() |
| |
| def _EnsurePublicKeyFile(self, file_node_name): |
| if file_node_name not in self._public_keys: |
| self._public_keys[file_node_name] = {} |
| self._AssertTypePublicKeys() |
| |
| def _ClearPublicKeys(self, file_node_name): |
| self._public_keys[file_node_name] = {} |
| self._AssertTypePublicKeys() |
| |
| def _OverridePublicKeys(self, ssh_keys, file_node_name): |
| assert isinstance(ssh_keys, dict) |
| self._ClearPublicKeys(file_node_name) |
| for key_node_uuid, node_keys in ssh_keys.items(): |
| assert isinstance(node_keys, list) |
| 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 |
| self._AssertTypePublicKeys() |
| |
| def _ReplaceOrAddPublicKeys(self, public_keys, file_node_name): |
| assert isinstance(public_keys, dict) |
| self._EnsurePublicKeyFile(file_node_name) |
| for key_node_uuid, keys in public_keys.items(): |
| assert isinstance(keys, list) |
| self._public_keys[file_node_name][key_node_uuid] = keys |
| self._AssertTypePublicKeys() |
| |
| def _RemovePublicKeys(self, public_keys, file_node_name): |
| assert isinstance(public_keys, dict) |
| 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] = [] |
| self._AssertTypePublicKeys() |
| |
| 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._ReplaceOrAddPublicKeys(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) |
| self._AssertTypePublicKeys() |
| |
| # 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 isinstance(keys, list) |
| assert self._master_node_name |
| self._AddAuthorizedKeys(self._master_node_name, keys) |
| self._AssertTypeAuthorizedKeys() |
| |
| 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 isinstance(keys, list) |
| assert self._master_node_name |
| self._RemoveAuthorizedKeys(self._master_node_name, keys) |
| self._AssertTypeAuthorizedKeys() |
| |
| 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 |
| assert isinstance(new_key, str) |
| key_dict = {new_uuid: [new_key]} |
| self._ReplaceOrAddPublicKeys(key_dict, self._master_node_name) |
| self._AssertTypePublicKeys() |
| |
| 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) |
| self._AssertTypePublicKeys() |
| |
| 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, keys in \ |
| self._public_keys[self._master_node_name].items(): |
| if key_node_uuid in target_uuids: |
| result_dict[key_node_uuid] = keys |
| self._AssertTypePublicKeys() |
| 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 isinstance(node_uuid, str) |
| assert isinstance(node_name, str) |
| 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] |
| self._AssertTypePublicKeys() |
| # pylint: enable=W0613 |