testutils: add keys to own 'authorized_keys' file
[ganeti-github.git] / test / py / testutils_ssh.py
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2010, 2013, 2015 Google Inc.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are
9 # met:
10 #
11 # 1. Redistributions of source code must retain the above copyright notice,
12 # this list of conditions and the following disclaimer.
13 #
14 # 2. Redistributions in binary form must reproduce the above copyright
15 # notice, this list of conditions and the following disclaimer in the
16 # documentation and/or other materials provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19 # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30
31 """Helper class to test ssh-related code."""
32
33 from ganeti import constants
34 from ganeti import pathutils
35 from ganeti import errors
36
37 from collections import namedtuple
38
39
40 class FakeSshFileManager(object):
41 """Class which 'fakes' the lowest layer of SSH key manipulation.
42
43 There are various operations which touch the nodes' SSH keys and their
44 respective key files (authorized_keys and ganeti_pub_keys). Those are
45 tedious to test as file operations have to be mocked on different levels
46 (direct access to the authorized_keys and ganeti_pub_keys) of the master
47 node, indirect access to those files of the non-master nodes (via the
48 ssh_update tool). In order to make unit tests of those operations more
49 readable and managable, we introduce this class, which mocks all
50 direct and indirect access to SSH key files on all nodes. This way,
51 the state of this FakeSshFileManager represents the state of a cluster's
52 nodes' SSH key files in a consise and easily accessible way.
53
54 """
55 def __init__(self):
56 # Dictionary mapping node name to node properties. The properties
57 # are a named tuple of (node_uuid, ssh_key, is_potential_master_candidate,
58 # is_master_candidate, is_master).
59 self._all_node_data = {}
60 # Dictionary emulating the authorized keys files of all nodes. The
61 # indices of the dictionary are the node names, the values are sets
62 # of keys (strings).
63 self._authorized_keys = {}
64 # Dictionary emulating the public keys file of all nodes. The indices
65 # of the dictionary are the node names where the public key file is
66 # 'located' (if it wasn't faked). The values of the dictionary are
67 # dictionaries itself. Each of those dictionaries is indexed by the
68 # node UUIDs mapping to a list of public keys.
69 self._public_keys = {} # dict of dicts
70 # Node name of the master node
71 self._master_node_name = None
72 # Dictionary mapping nodes by name to number of retries where 'RunCommand'
73 # succeeds. For example if set to '3', RunCommand will fail two times when
74 # called for this node before it succeeds in the 3rd retry.
75 self._max_retries = {}
76 # Dictionary mapping nodes by name to number of retries which
77 # 'RunCommand' has already carried out.
78 self._retries = {}
79
80 self._AssertTypePublicKeys()
81 self._AssertTypeAuthorizedKeys()
82
83 _NodeInfo = namedtuple(
84 "NodeInfo",
85 ["uuid",
86 "key",
87 "is_potential_master_candidate",
88 "is_master_candidate",
89 "is_master"])
90
91 def _SetMasterNodeName(self):
92 self._master_node_name = [name for name, node_info
93 in self._all_node_data.items()
94 if node_info.is_master][0]
95
96 def GetMasterNodeName(self):
97 return self._master_node_name
98
99 def _CreateNodeDict(self, num_nodes, num_pot_mcs, num_mcs):
100 """Creates a dictionary of all nodes and their properties."""
101
102 self._all_node_data = {}
103 for i in range(num_nodes):
104 name = "node_name_%i" % i
105 uuid = "node_uuid_%i" % i
106 key = "key%s" % i
107 self._public_keys[name] = {}
108 self._authorized_keys[name] = set()
109
110 pot_mc = i < num_pot_mcs
111 mc = i < num_mcs
112 master = i == num_mcs / 2
113
114 self._all_node_data[name] = self._NodeInfo(uuid, key, pot_mc, mc, master)
115
116 self._AssertTypePublicKeys()
117 self._AssertTypeAuthorizedKeys()
118
119 def _FillPublicKeyOfOneNode(self, receiving_node_name):
120 node_info = self._all_node_data[receiving_node_name]
121 # Nodes which are not potential master candidates receive no keys
122 if not node_info.is_potential_master_candidate:
123 return
124 for node_info in self._all_node_data.values():
125 if node_info.is_potential_master_candidate:
126 self._public_keys[receiving_node_name][node_info.uuid] = [node_info.key]
127
128 def _FillAuthorizedKeyOfOneNode(self, receiving_node_name):
129 for node_name, node_info in self._all_node_data.items():
130 if node_info.is_master_candidate \
131 or node_name == receiving_node_name:
132 self._authorized_keys[receiving_node_name].add(node_info.key)
133
134 def InitAllNodes(self, num_nodes, num_pot_mcs, num_mcs):
135 """Initializes the entire state of the cluster wrt SSH keys.
136
137 @type num_nodes: int
138 @param num_nodes: number of nodes in the cluster
139 @type num_pot_mcs: int
140 @param num_pot_mcs: number of potential master candidates in the cluster
141 @type num_mcs: in
142 @param num_mcs: number of master candidates in the cluster.
143
144 """
145 self._public_keys = {}
146 self._authorized_keys = {}
147 self._CreateNodeDict(num_nodes, num_pot_mcs, num_mcs)
148 for node in self._all_node_data.keys():
149 self._FillPublicKeyOfOneNode(node)
150 self._FillAuthorizedKeyOfOneNode(node)
151 self._SetMasterNodeName()
152 self._AssertTypePublicKeys()
153 self._AssertTypeAuthorizedKeys()
154
155 def SetMaxRetries(self, node_name, retries):
156 """Set the number of unsuccessful retries of 'RunCommand' per node.
157
158 @type node_name: string
159 @param node_name: name of the node
160 @type retries: integer
161 @param retries: number of unsuccessful retries
162
163 """
164 self._max_retries[node_name] = retries
165
166 def GetSshPortMap(self, port):
167 """Creates a SSH port map with all nodes mapped to the given port.
168
169 @type port: int
170 @param port: SSH port number for all nodes
171
172 """
173 port_map = {}
174 for node in self._all_node_data.keys():
175 port_map[node] = port
176 return port_map
177
178 def GetAllNodeNames(self):
179 return self._all_node_data.keys()
180
181 def GetAllPotentialMasterCandidateNodeNames(self):
182 return [name for name, node_info
183 in self._all_node_data.items()
184 if node_info.is_potential_master_candidate]
185
186 def GetAllMasterCandidateUuids(self):
187 return [node_info.uuid for node_info
188 in self._all_node_data.values() if node_info.is_master_candidate]
189
190 def GetAllPurePotentialMasterCandidates(self):
191 """Get the potential master candidates which are not master candidates."""
192 return [(name, node_info) for name, node_info
193 in self._all_node_data.items()
194 if node_info.is_potential_master_candidate and
195 not node_info.is_master_candidate]
196
197 def GetAllMasterCandidates(self):
198 return [(name, node_info) for name, node_info
199 in self._all_node_data.items() if node_info.is_master_candidate]
200
201 def GetAllNormalNodes(self):
202 return [(name, node_info) for name, node_info
203 in self._all_node_data.items() if not node_info.is_master_candidate
204 and not node_info.is_potential_master_candidate]
205
206 def GetPublicKeysOfNode(self, node):
207 return self._public_keys[node]
208
209 def GetAuthorizedKeysOfNode(self, node):
210 return self._authorized_keys[node]
211
212 def SetOrAddNode(self, name, uuid, key, pot_mc, mc, master):
213 """Adds a new node to the state of the file manager.
214
215 This is necessary when testing to add new nodes to the cluster. Otherwise
216 this new node's state would not be evaluated properly with the assertion
217 functions.
218
219 @type name: string
220 @param name: name of the new node
221 @type uuid: string
222 @param uuid: UUID of the new node
223 @type key: string
224 @param key: SSH key of the new node
225 @type pot_mc: boolean
226 @param pot_mc: whether the new node is a potential master candidate
227 @type mc: boolean
228 @param mc: whether the new node is a master candidate
229 @type master: boolean
230 @param master: whether the new node is the master
231
232 """
233 self._all_node_data[name] = self._NodeInfo(uuid, key, pot_mc, mc, master)
234 if name not in self._authorized_keys:
235 self._authorized_keys[name] = set()
236 if mc:
237 self._authorized_keys[name].add(key)
238 if name not in self._public_keys:
239 self._public_keys[name] = {}
240 self._AssertTypePublicKeys()
241 self._AssertTypeAuthorizedKeys()
242
243 def NodeHasPublicKey(self, file_node_name, key_node_uuid, key):
244 """Checks whether a node has another node's public key.
245
246 @type file_node_name: string
247 @param file_node_name: name of the node whose public key file is inspected
248 @type key_node_uuid: string
249 @param key_node_uuid: UUID of the node whose key is checked for
250 @rtype: boolean
251 @return: True if the key_node's UUID is found with the machting key 'key'
252
253 """
254 for (node_uuid, pub_keys) in self._public_keys[file_node_name].items():
255 if key in pub_keys and key_node_uuid == node_uuid:
256 return True
257 return False
258
259 def NodeHasAuthorizedKey(self, file_node_name, key):
260 """Checks whether a node has a particular key in its authorized_keys file.
261
262 @type file_node_name: string
263 @param file_node_name: name of the node whose authorized_key file is
264 inspected
265 @type key: string
266 @param key: key which is expected to be found in the node's authorized_key
267 file
268 @rtype: boolean
269 @return: True if the key is found in the node's authorized_key file
270
271 """
272 return key in self._authorized_keys[file_node_name]
273
274 def AssertNodeSetOnlyHasAuthorizedKey(self, node_set, query_node_key):
275 """Check if nodes in the given set only have a particular authorized key.
276
277 @type node_set: list of strings
278 @param node_set: list of nodes who are supposed to have the key
279 @type query_node_key: string
280 @param query_node_key: key which is looked for
281
282 """
283 assert isinstance(node_set, list)
284 for node_name in self._all_node_data.keys():
285 if node_name in node_set:
286 if not self.NodeHasAuthorizedKey(node_name, query_node_key):
287 raise Exception("Node '%s' does not have authorized key '%s'."
288 % (node_name, query_node_key))
289 else:
290 if self.NodeHasAuthorizedKey(node_name, query_node_key):
291 raise Exception("Node '%s' has authorized key '%s' although it"
292 " should not." % (node_name, query_node_key))
293
294 def AssertAllNodesHaveAuthorizedKey(self, key):
295 """Check if all nodes have a particular key in their auth. keys file.
296
297 @type key: string
298 @param key: key exptected to be present in all node's authorized_keys file
299 @raise Exception: if a node does not have the authorized key.
300
301 """
302 self.AssertNodeSetOnlyHasAuthorizedKey(self._all_node_data.keys(), key)
303
304 def AssertNoNodeHasAuthorizedKey(self, key):
305 """Check if none of the nodes has a particular key in their auth. keys file.
306
307 @type key: string
308 @param key: key exptected to be present in all node's authorized_keys file
309 @raise Exception: if a node *does* have the authorized key.
310
311 """
312 self.AssertNodeSetOnlyHasAuthorizedKey([], key)
313
314 def AssertNodeSetOnlyHasPublicKey(self, node_set, query_node_uuid,
315 query_node_key):
316 """Check if nodes in the given set only have a particular public key.
317
318 @type node_set: list of strings
319 @param node_set: list of nodes who are supposed to have the key
320 @type query_node_uuid: string
321 @param query_node_uuid: uuid of the node whose key is looked for
322 @type query_node_key: string
323 @param query_node_key: key which is looked for
324
325 """
326 for node_name in self._all_node_data.keys():
327 if node_name in node_set:
328 if not self.NodeHasPublicKey(node_name, query_node_uuid,
329 query_node_key):
330 raise Exception("Node '%s' does not have public key '%s' of node"
331 " '%s'." % (node_name, query_node_key,
332 query_node_uuid))
333 else:
334 if self.NodeHasPublicKey(node_name, query_node_uuid, query_node_key):
335 raise Exception("Node '%s' has public key '%s' of node"
336 " '%s' although it should not."
337 % (node_name, query_node_key, query_node_uuid))
338
339 def AssertNoNodeHasPublicKey(self, uuid, key):
340 """Check if none of the nodes have the given public key in their file.
341
342 @type uuid: string
343 @param uuid: UUID of the node whose key is looked for
344 @raise Exception: if a node *does* have the public key.
345
346 """
347 self.AssertNodeSetOnlyHasPublicKey([], uuid, key)
348
349 def AssertPotentialMasterCandidatesOnlyHavePublicKey(self, query_node_name):
350 """Checks if the node's key is on all potential master candidates only.
351
352 This ensures that the node's key is in all public key files of all
353 potential master candidates, and it also checks whether the key is
354 *not* in all other nodes's key files.
355
356 @param query_node_name: name of the node whose key is expected to be
357 in the public key file of all potential master
358 candidates
359 @type query_node_name: string
360 @raise Exception: when a potential master candidate does not have
361 the public key or a normal node *does* have a public key.
362
363 """
364 query_node_uuid, query_node_key, _, _, _ = \
365 self._all_node_data[query_node_name]
366 potential_master_candidates = self.GetAllPotentialMasterCandidateNodeNames()
367 self.AssertNodeSetOnlyHasPublicKey(
368 potential_master_candidates, query_node_uuid, query_node_key)
369
370 def _AssertTypePublicKeys(self):
371 """Asserts that the public key dictionary has the right types.
372
373 This is helpful as an invariant that shall not be violated during the
374 tests due to type errors.
375
376 """
377 assert isinstance(self._public_keys, dict)
378 for node_file, pub_keys in self._public_keys.items():
379 assert isinstance(node_file, str)
380 assert isinstance(pub_keys, dict)
381 for node_key, keys in pub_keys.items():
382 assert isinstance(node_key, str)
383 assert isinstance(keys, list)
384 for key in keys:
385 assert isinstance(key, str)
386
387 def _AssertTypeAuthorizedKeys(self):
388 """Asserts that the authorized keys dictionary has the right types.
389
390 This is useful to check as an invariant that is not supposed to be violated
391 during the tests.
392
393 """
394 assert isinstance(self._authorized_keys, dict)
395 for node_file, auth_keys in self._authorized_keys.items():
396 assert isinstance(node_file, str)
397 assert isinstance(auth_keys, set)
398 for key in auth_keys:
399 assert isinstance(key, str)
400
401 # Disabling a pylint warning about unused parameters. Those need
402 # to be here to properly mock the real methods.
403 # pylint: disable=W0613
404 def RunCommand(self, cluster_name, node, base_cmd, port, data,
405 debug=False, verbose=False, use_cluster_key=False,
406 ask_key=False, strict_host_check=False,
407 ensure_version=False):
408 """This emulates ssh.RunSshCmdWithStdin calling ssh_update.
409
410 While in real SSH operations, ssh.RunSshCmdWithStdin is called
411 with the command ssh_update to manipulate a remote node's SSH
412 key files (authorized_keys and ganeti_pub_key) file, this method
413 emulates the operation by manipulating only its internal dictionaries
414 of SSH keys. No actual key files of any node is touched.
415
416 """
417 if node in self._max_retries:
418 if node not in self._retries:
419 self._retries[node] = 0
420 self._retries[node] += 1
421 if self._retries[node] < self._max_retries[node]:
422 raise errors.OpExecError("(Fake) SSH connection to node '%s' failed."
423 % node)
424
425 assert base_cmd == pathutils.SSH_UPDATE
426
427 if constants.SSHS_SSH_AUTHORIZED_KEYS in data:
428 instructions_auth = data[constants.SSHS_SSH_AUTHORIZED_KEYS]
429 self._HandleAuthorizedKeys(instructions_auth, node)
430 if constants.SSHS_SSH_PUBLIC_KEYS in data:
431 instructions_pub = data[constants.SSHS_SSH_PUBLIC_KEYS]
432 self._HandlePublicKeys(instructions_pub, node)
433 # pylint: enable=W0613
434
435 def _EnsureAuthKeyFile(self, file_node_name):
436 if file_node_name not in self._authorized_keys:
437 self._authorized_keys[file_node_name] = set()
438 self._AssertTypePublicKeys()
439 self._AssertTypeAuthorizedKeys()
440
441 def _AddAuthorizedKeys(self, file_node_name, ssh_keys):
442 """Mocks adding the given keys to the authorized_keys file."""
443 assert isinstance(ssh_keys, list)
444 self._EnsureAuthKeyFile(file_node_name)
445 for key in ssh_keys:
446 self._authorized_keys[file_node_name].add(key)
447 self._AssertTypePublicKeys()
448 self._AssertTypeAuthorizedKeys()
449
450 def _RemoveAuthorizedKeys(self, file_node_name, keys):
451 """Mocks removing the keys from authorized_keys on the given node.
452
453 @param keys: list of ssh keys
454 @type keys: list of strings
455
456 """
457 self._EnsureAuthKeyFile(file_node_name)
458 self._authorized_keys[file_node_name] = \
459 set([k for k in self._authorized_keys[file_node_name] if k not in keys])
460 self._AssertTypeAuthorizedKeys()
461
462 def _HandleAuthorizedKeys(self, instructions, node):
463 (action, authorized_keys) = instructions
464 ssh_key_sets = authorized_keys.values()
465 if action == constants.SSHS_ADD:
466 for ssh_keys in ssh_key_sets:
467 self._AddAuthorizedKeys(node, ssh_keys)
468 elif action == constants.SSHS_REMOVE:
469 for ssh_keys in ssh_key_sets:
470 self._RemoveAuthorizedKeys(node, ssh_keys)
471 else:
472 raise Exception("Unsupported action: %s" % action)
473 self._AssertTypeAuthorizedKeys()
474
475 def _EnsurePublicKeyFile(self, file_node_name):
476 if file_node_name not in self._public_keys:
477 self._public_keys[file_node_name] = {}
478 self._AssertTypePublicKeys()
479
480 def _ClearPublicKeys(self, file_node_name):
481 self._public_keys[file_node_name] = {}
482 self._AssertTypePublicKeys()
483
484 def _OverridePublicKeys(self, ssh_keys, file_node_name):
485 assert isinstance(ssh_keys, dict)
486 self._ClearPublicKeys(file_node_name)
487 for key_node_uuid, node_keys in ssh_keys.items():
488 assert isinstance(node_keys, list)
489 if key_node_uuid in self._public_keys[file_node_name]:
490 raise Exception("Duplicate node in ssh_update data.")
491 self._public_keys[file_node_name][key_node_uuid] = node_keys
492 self._AssertTypePublicKeys()
493
494 def _ReplaceOrAddPublicKeys(self, public_keys, file_node_name):
495 assert isinstance(public_keys, dict)
496 self._EnsurePublicKeyFile(file_node_name)
497 for key_node_uuid, keys in public_keys.items():
498 assert isinstance(keys, list)
499 self._public_keys[file_node_name][key_node_uuid] = keys
500 self._AssertTypePublicKeys()
501
502 def _RemovePublicKeys(self, public_keys, file_node_name):
503 assert isinstance(public_keys, dict)
504 self._EnsurePublicKeyFile(file_node_name)
505 for key_node_uuid, _ in public_keys.items():
506 if key_node_uuid in self._public_keys[file_node_name]:
507 self._public_keys[file_node_name][key_node_uuid] = []
508 self._AssertTypePublicKeys()
509
510 def _HandlePublicKeys(self, instructions, node):
511 (action, public_keys) = instructions
512 if action == constants.SSHS_OVERRIDE:
513 self._OverridePublicKeys(public_keys, node)
514 elif action == constants.SSHS_ADD:
515 self._ReplaceOrAddPublicKeys(public_keys, node)
516 elif action == constants.SSHS_REPLACE_OR_ADD:
517 self._ReplaceOrAddPublicKeys(public_keys, node)
518 elif action == constants.SSHS_REMOVE:
519 self._RemovePublicKeys(public_keys, node)
520 elif action == constants.SSHS_CLEAR:
521 self._ClearPublicKeys(node)
522 else:
523 raise Exception("Unsupported action: %s." % action)
524 self._AssertTypePublicKeys()
525
526 # pylint: disable=W0613
527 def AddAuthorizedKeys(self, file_obj, keys):
528 """Emulates ssh.AddAuthorizedKeys on the master node.
529
530 Instead of actually mainpulating the authorized_keys file, this method
531 keeps the state of the file in a dictionary in memory.
532
533 @see: C{ssh.AddAuthorizedKeys}
534
535 """
536 assert isinstance(keys, list)
537 assert self._master_node_name
538 self._AddAuthorizedKeys(self._master_node_name, keys)
539 self._AssertTypeAuthorizedKeys()
540
541 def RemoveAuthorizedKeys(self, file_name, keys):
542 """Emulates ssh.RemoveAuthorizeKeys on the master node.
543
544 Instead of actually mainpulating the authorized_keys file, this method
545 keeps the state of the file in a dictionary in memory.
546
547 @see: C{ssh.RemoveAuthorizedKeys}
548
549 """
550 assert isinstance(keys, list)
551 assert self._master_node_name
552 self._RemoveAuthorizedKeys(self._master_node_name, keys)
553 self._AssertTypeAuthorizedKeys()
554
555 def AddPublicKey(self, new_uuid, new_key, **kwargs):
556 """Emulates ssh.AddPublicKey on the master node.
557
558 Instead of actually mainpulating the authorized_keys file, this method
559 keeps the state of the file in a dictionary in memory.
560
561 @see: C{ssh.AddPublicKey}
562
563 """
564 assert self._master_node_name
565 assert isinstance(new_key, str)
566 key_dict = {new_uuid: [new_key]}
567 self._ReplaceOrAddPublicKeys(key_dict, self._master_node_name)
568 self._AssertTypePublicKeys()
569
570 def RemovePublicKey(self, target_uuid, **kwargs):
571 """Emulates ssh.RemovePublicKey on the master node.
572
573 Instead of actually mainpulating the authorized_keys file, this method
574 keeps the state of the file in a dictionary in memory.
575
576 @see: {ssh.RemovePublicKey}
577
578 """
579 assert self._master_node_name
580 key_dict = {target_uuid: []}
581 self._RemovePublicKeys(key_dict, self._master_node_name)
582 self._AssertTypePublicKeys()
583
584 def QueryPubKeyFile(self, target_uuids, **kwargs):
585 """Emulates ssh.QueryPubKeyFile on the master node.
586
587 Instead of actually mainpulating the authorized_keys file, this method
588 keeps the state of the file in a dictionary in memory.
589
590 @see: C{ssh.QueryPubKey}
591
592 """
593 assert self._master_node_name
594 all_keys = target_uuids is None
595 if all_keys:
596 return self._public_keys[self._master_node_name]
597
598 if isinstance(target_uuids, str):
599 target_uuids = [target_uuids]
600 result_dict = {}
601 for key_node_uuid, keys in \
602 self._public_keys[self._master_node_name].items():
603 if key_node_uuid in target_uuids:
604 result_dict[key_node_uuid] = keys
605 self._AssertTypePublicKeys()
606 return result_dict
607
608 def ReplaceNameByUuid(self, node_uuid, node_name, **kwargs):
609 """Emulates ssh.ReplaceNameByUuid on the master node.
610
611 Instead of actually mainpulating the authorized_keys file, this method
612 keeps the state of the file in a dictionary in memory.
613
614 @see: C{ssh.ReplacenameByUuid}
615
616 """
617 assert isinstance(node_uuid, str)
618 assert isinstance(node_name, str)
619 assert self._master_node_name
620 if node_name in self._public_keys[self._master_node_name]:
621 self._public_keys[self._master_node_name][node_uuid] = \
622 self._public_keys[self._master_node_name][node_name][:]
623 del self._public_keys[self._master_node_name][node_name]
624 self._AssertTypePublicKeys()
625 # pylint: enable=W0613