On Wed, Jul 30, 2014 at 11:31 AM, 'Helga Velroyen' via ganeti-devel <
ganeti-devel@googlegroups.com> wrote:

> This patch initializes the "ganeti_pub_keys" file on
> cluster initialization and adds the master's key to it.
> On node-add, the key file is queried for the keys of
> the master candidates and those are transferred to the
> new node and added to its "authorized_keys" file.
>
> Signed-off-by: Helga Velroyen <hel...@google.com>
> ---
>  lib/bootstrap.py                                   |  4 ++
>  lib/client/gnt_node.py                             | 15 ++++++-
>  lib/ssh.py                                         | 51
> ++++++++++++++++++----
>  lib/tools/prepare_node_join.py                     | 40 +++++++++++------
>  src/Ganeti/Constants.hs                            |  3 ++
>  test/py/ganeti.ssh_unittest.py                     | 20 +++++++++
>  test/py/ganeti.tools.prepare_node_join_unittest.py | 14 +++++-
>  7 files changed, 122 insertions(+), 25 deletions(-)
>
> diff --git a/lib/bootstrap.py b/lib/bootstrap.py
> index 9f7e681..0160f6b 100644
> --- a/lib/bootstrap.py
> +++ b/lib/bootstrap.py
> @@ -27,6 +27,7 @@ import os
>  import os.path
>  import re
>  import logging
> +import tempfile
>  import time
>  import tempfile
>
> @@ -836,6 +837,9 @@ def InitCluster(cluster_name, mac_prefix, # pylint:
> disable=R0913, R0914
>    cfg.Update(cfg.GetClusterInfo(), logging.error)
>    ssconf.WriteSsconfFiles(cfg.GetSsconfValues())
>
> +  master_uuid = cfg.GetMasterNode()
> +  if modify_ssh_setup:
> +    ssh.InitPubKeyFile(master_uuid)
>    # set up the inter-node password and certificate
>    _InitGanetiServerSetup(hostname.name)
>
> diff --git a/lib/client/gnt_node.py b/lib/client/gnt_node.py
> index d10469f..d77d031 100644
> --- a/lib/client/gnt_node.py
> +++ b/lib/client/gnt_node.py
> @@ -183,7 +183,7 @@ def _ReadSshKeys(keyfiles, _tostderr_fn=ToStderr):
>    return result
>
>
> -def _SetupSSH(options, cluster_name, node, ssh_port):
> +def _SetupSSH(options, cluster_name, node, ssh_port, cl):
>    """Configures a destination node's SSH daemon.
>
>    @param options: Command line options
> @@ -193,8 +193,18 @@ def _SetupSSH(options, cluster_name, node, ssh_port):
>    @param node: Destination node name
>    @type ssh_port: int
>    @param ssh_port: Destination node ssh port
> +  @param cl: luxi client
>
>    """
> +  # Retrieve the list of master and master candidates
> +  candidate_filter = ["|", ["=", "role", "M"], ["=", "role", "C"]]
> +  result = cl.Query(constants.QR_NODE, ["uuid"], candidate_filter)
> +  if len(result.data) < 1:
> +    raise errors.OpPrereqError("No master or master candidate nodes are"
> +                               " found.")
> +  candidates = [uuid for (_, uuid) in result.data[0]]
> +  candidate_keys = ssh.QueryPubKeyFile(candidates)
> +
>    if options.force_join:
>      ToStderr("The \"--force-join\" option is no longer supported and will
> be"
>               " ignored.")
> @@ -214,6 +224,7 @@ def _SetupSSH(options, cluster_name, node, ssh_port):
>      constants.SSHS_NODE_DAEMON_CERTIFICATE: cert_pem,
>      constants.SSHS_SSH_HOST_KEY: host_keys,
>      constants.SSHS_SSH_ROOT_KEY: root_keys,
> +    constants.SSHS_SSH_AUTHORIZED_KEYS: candidate_keys,
>      }
>
>    bootstrap.RunNodeSetupCmd(cluster_name, node,
> pathutils.PREPARE_NODE_JOIN,
> @@ -289,7 +300,7 @@ def AddNode(opts, args):
>               "and grant full intra-cluster ssh root access to/from it\n",
> node)
>
>    if opts.node_setup:
> -    _SetupSSH(opts, cluster_name, node, ssh_port)
> +    _SetupSSH(opts, cluster_name, node, ssh_port, cl)
>
>    bootstrap.SetupNodeDaemon(opts, cluster_name, node, ssh_port)
>
> diff --git a/lib/ssh.py b/lib/ssh.py
> index 61c7d2e..34d709d 100644
> --- a/lib/ssh.py
> +++ b/lib/ssh.py
> @@ -132,16 +132,16 @@ def _SplitSshKey(key):
>      return (True, parts)
>
>
> -def AddAuthorizedKey(file_obj, key):
> -  """Adds an SSH public key to an authorized_keys file.
> +def AddAuthorizedKeys(file_obj, keys):
> +  """Adds a list of SSH public key to an authorized_keys file.
>
>    @type file_obj: str or file handle
>    @param file_obj: path to authorized_keys file
> -  @type key: str
> -  @param key: string containing key
> +  @type keys: list of str
> +  @param keys: list of strings containing keys
>
>    """
> -  key_fields = _SplitSshKey(key)
> +  key_field_list = [(key, _SplitSshKey(key)) for key in keys]
>
>    if isinstance(file_obj, basestring):
>      f = open(file_obj, "a+")
> @@ -152,19 +152,39 @@ def AddAuthorizedKey(file_obj, key):
>      nl = True
>      for line in f:
>        # Ignore whitespace changes
> -      if _SplitSshKey(line) == key_fields:
> +      line_key = _SplitSshKey(line)
> +      key_found = False
> +      for (key, split_key) in key_field_list:
> +        if line_key == split_key:
> +          key_found = True
> +          key_field_list.remove((key, split_key))
> +          break
> +      if key_found:
>          break
>        nl = line.endswith("\n")
>      else:
>        if not nl:
>          f.write("\n")
> -      f.write(key.rstrip("\r\n"))
> -      f.write("\n")
> +      for (key, _) in key_field_list:
> +        f.write(key.rstrip("\r\n"))
> +        f.write("\n")
>        f.flush()
>

If we're adding 2 keys and just one of them is already present, the other
one won't be added. So I'd suggest to just keep a list/dictionary of those
that aren't present and add them at the end.



>    finally:
>      f.close()
>
>
> +def AddAuthorizedKey(file_obj, key):
> +  """Adds an SSH public key to an authorized_keys file.
> +
> +  @type file_obj: str or file handle
> +  @param file_obj: path to authorized_keys file
> +  @type key: str
> +  @param key: string containing key
> +
> +  """
> +  AddAuthorizedKeys(file_obj, [key])
> +
> +
>  def RemoveAuthorizedKey(file_name, key):
>    """Removes an SSH public key from an authorized_keys file.
>
> @@ -557,6 +577,21 @@ def InitSSHSetup(error_fn=errors.OpPrereqError):
>    AddAuthorizedKey(auth_keys, utils.ReadFile(pub_key))
>
>
> +def InitPubKeyFile(master_uuid, key_file=pathutils.SSH_PUB_KEYS):
> +  """Creates the public key file and adds the master node's SSH key.
> +
> +  @type master_uuid: str
> +  @param master_uuid: the master node's UUID
> +  @type key_file: str
> +  @param key_file: name of the file containing the public keys
> +
> +  """
> +  _, pub_key, _ = GetUserFiles(constants.SSH_LOGIN_USER)
> +  utils.WriteFile(key_file, data="", mode=0600)
> +  key = utils.ReadFile(pub_key)
> +  AddPublicKey(master_uuid, key, key_file=key_file)
> +
> +
>  class SshRunner:
>    """Wrapper for SSH commands.
>
> diff --git a/lib/tools/prepare_node_join.py
> b/lib/tools/prepare_node_join.py
> index 789c872..7d96c6a 100644
> --- a/lib/tools/prepare_node_join.py
> +++ b/lib/tools/prepare_node_join.py
> @@ -55,6 +55,8 @@ _DATA_CHECK = ht.TStrictDict(False, True, {
>    constants.SSHS_NODE_DAEMON_CERTIFICATE: ht.TNonEmptyString,
>    constants.SSHS_SSH_HOST_KEY: _SSH_KEY_LIST,
>    constants.SSHS_SSH_ROOT_KEY: _SSH_KEY_LIST,
> +  constants.SSHS_SSH_AUTHORIZED_KEYS:
> +    ht.TDictOf(ht.TNonEmptyString, ht.TListOf(ht.TNonEmptyString)),
>    })
>
>
> @@ -213,20 +215,30 @@ def UpdateSshRoot(data, dry_run, _homedir_fn=None):
>
>    """
>    keys = data.get(constants.SSHS_SSH_ROOT_KEY)
> -  if not keys:
> -    return
> -
> -  (auth_keys_file, keyfiles) = \
> -    ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=True,
> -                        _homedir_fn=_homedir_fn)
> -
> -  _UpdateKeyFiles(keys, dry_run, keyfiles)
> -
> -  if dry_run:
> -    logging.info("This is a dry run, not modifying %s", auth_keys_file)
> -  else:
> -    for (_, _, public_key) in keys:
> -      ssh.AddAuthorizedKey(auth_keys_file, public_key)
> +  authorized_keys = data.get(constants.SSHS_SSH_AUTHORIZED_KEYS)
> +
> +  if keys or authorized_keys:
> +    (auth_keys_file, keyfiles) = \
> +      ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=True,
> +                          _homedir_fn=_homedir_fn)
> +
> +    if keys:
> +      _UpdateKeyFiles(keys, dry_run, keyfiles)
> +
> +      if dry_run:
> +        logging.info("This is a dry run, not modifying %s",
> auth_keys_file)
> +      else:
> +        for (_, _, public_key) in keys:
> +          ssh.AddAuthorizedKey(auth_keys_file, public_key)
> +
> +    if authorized_keys:
> +      if dry_run:
> +        logging.info("This is a dry run, not modifying %s",
> auth_keys_file)
> +      else:
> +        all_authorized_keys = []
> +        for keys in authorized_keys.values():
> +          all_authorized_keys += keys
> +        ssh.AddAuthorizedKeys(auth_keys_file, all_authorized_keys)
>
>
Since it seems 'keys' is completely removed later in the patch series, it'd
be better to remove it in this patch directly (unless it's still used
somehow here).


>
>  def LoadData(raw):
> diff --git a/src/Ganeti/Constants.hs b/src/Ganeti/Constants.hs
> index 2da4858..e29526b 100644
> --- a/src/Ganeti/Constants.hs
> +++ b/src/Ganeti/Constants.hs
> @@ -4450,6 +4450,9 @@ sshsSshHostKey = "ssh_host_key"
>  sshsSshRootKey :: String
>  sshsSshRootKey = "ssh_root_key"
>
> +sshsSshAuthorizedKeys :: String
> +sshsSshAuthorizedKeys = "authorized_keys"
> +
>  sshsNodeDaemonCertificate :: String
>  sshsNodeDaemonCertificate = "node_daemon_certificate"
>
> diff --git a/test/py/ganeti.ssh_unittest.py b/test/py/
> ganeti.ssh_unittest.py
> index cd8da19..09d3169 100755
> --- a/test/py/ganeti.ssh_unittest.py
> +++ b/test/py/ganeti.ssh_unittest.py
> @@ -215,6 +215,26 @@ class TestSshKeys(testutils.GanetiTestCase):
>        'command="/usr/bin/fooserver -t --verbose",from="198.51.100.4"'
>        " ssh-dss AAAAB3NzaC1w520smc01ms0jfJs22 root@key-b\n")
>
> +  def testAddingNewKeys(self):
> +    ssh.AddAuthorizedKeys(self.tmpname,
> +                          ["ssh-dss AAAAB3NzaC1kc3MAAACB root@test"])
> +    self.assertFileContent(self.tmpname,
> +      "ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@key-a\n"
> +      'command="/usr/bin/fooserver -t --verbose",from="198.51.100.4"'
> +      " ssh-dss AAAAB3NzaC1w520smc01ms0jfJs22 root@key-b\n"
> +      "ssh-dss AAAAB3NzaC1kc3MAAACB root@test\n")
> +
> +    ssh.AddAuthorizedKeys(self.tmpname,
> +                          ["ssh-dss AAAAB3asdfasdfaYTUCB laracroft@test",
> +                           "ssh-dss AasdfliuobaosfMAAACB frodo@test"])
> +    self.assertFileContent(self.tmpname,
> +      "ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@key-a\n"
> +      'command="/usr/bin/fooserver -t --verbose",from="198.51.100.4"'
> +      " ssh-dss AAAAB3NzaC1w520smc01ms0jfJs22 root@key-b\n"
> +      "ssh-dss AAAAB3NzaC1kc3MAAACB root@test\n"
> +      "ssh-dss AAAAB3asdfasdfaYTUCB laracroft@test\n"
> +      "ssh-dss AasdfliuobaosfMAAACB frodo@test\n")
> +
>
>  class TestPublicSshKeys(testutils.GanetiTestCase):
>    """Test case for the handling of the list of public ssh keys."""
> diff --git a/test/py/ganeti.tools.prepare_node_join_unittest.py b/test/py/
> ganeti.tools.prepare_node_join_unittest.py
> index 54d1d62..fe4ff26 100755
> --- a/test/py/ganeti.tools.prepare_node_join_unittest.py
> +++ b/test/py/ganeti.tools.prepare_node_join_unittest.py
> @@ -55,10 +55,22 @@ class TestLoadData(unittest.TestCase):
>      raw = serializer.DumpJson([])
>      self.assertRaises(errors.ParseError, prepare_node_join.LoadData, raw)
>
> -  def testValidData(self):
> +  def testEmptyDict(self):
>      raw = serializer.DumpJson({})
>      self.assertEqual(prepare_node_join.LoadData(raw), {})
>
> +  def testValidData(self):
> +    key_list = [[constants.SSHK_DSA, "private foo", "public bar"]]
> +    data_dict = {
> +      constants.SSHS_CLUSTER_NAME: "Skynet",
> +      constants.SSHS_SSH_HOST_KEY: key_list,
> +      constants.SSHS_SSH_ROOT_KEY: key_list,
> +      constants.SSHS_SSH_AUTHORIZED_KEYS:
> +        {"nodeuuid01234": ["foo"],
> +         "nodeuuid56789": ["bar"]}}
> +    raw = serializer.DumpJson(data_dict)
> +    self.assertEqual(prepare_node_join.LoadData(raw), data_dict)
> +
>
>  class TestVerifyCertificate(testutils.GanetiTestCase):
>    def setUp(self):
> --
> 2.0.0.526.g5318336
>
>
Rest LGTM

Reply via email to