On Thu, Jul 31, 2014 at 3:36 PM, Petr Pudlák <pud...@google.com> wrote:

>
>
>
> On Wed, Jul 30, 2014 at 11:31 AM, 'Helga Velroyen' via ganeti-devel <
> ganeti-devel@googlegroups.com> wrote:
>
>> In order to update the 'ganeti_pub_keys' and the
>> 'authorized_keys' files of various nodes via SSH, we
>> introduce the tool 'ssh_update'. It works similar to the
>> tool 'prepare_node_join', which is also a tool invoked
>> via SSH on a remote note.
>>
>> This patch includes some refactoring to reuse code from
>> the 'prepare_node_join' tool and provides unit tests as
>> well. Note that the actual invocation of the 'ssh_update'
>> tool will be done in later patches of this series.
>>
>> Signed-off-by: Helga Velroyen <hel...@google.com>
>> ---
>>  .gitignore                                         |   1 +
>>  Makefile.am                                        |  13 +-
>>  lib/pathutils.py                                   |   1 +
>>  lib/tools/common.py                                | 100 +++++++++++++
>>  lib/tools/prepare_node_join.py                     |  79 +----------
>>  lib/tools/ssh_update.py                            | 154
>> +++++++++++++++++++++
>>  test/py/ganeti.tools.prepare_node_join_unittest.py |  38 ++---
>>  test/py/ganeti.tools.ssh_update_unittest.py        | 123
>> ++++++++++++++++
>>  8 files changed, 414 insertions(+), 95 deletions(-)
>>  create mode 100644 lib/tools/common.py
>>  create mode 100644 lib/tools/ssh_update.py
>>  create mode 100755 test/py/ganeti.tools.ssh_update_unittest.py
>>
>> diff --git a/.gitignore b/.gitignore
>> index bbc5402..7e90385 100644
>> --- a/.gitignore
>> +++ b/.gitignore
>> @@ -127,6 +127,7 @@
>>  /tools/node-cleanup
>>  /tools/node-daemon-setup
>>  /tools/prepare-node-join
>> +/tools/ssh-update
>>
>>  # scripts
>>  /scripts/gnt-backup
>> diff --git a/Makefile.am b/Makefile.am
>> index cd7f20e..73be823 100644
>> --- a/Makefile.am
>> +++ b/Makefile.am
>> @@ -298,6 +298,8 @@ CLEANFILES = \
>>         tools/net-common \
>>         tools/users-setup \
>>         tools/vcluster-setup \
>> +       tools/prepare-node-join \
>> +       tools/ssh-update \
>>         stamp-directories \
>>         stamp-srclinks \
>>         $(nodist_pkgpython_PYTHON) \
>> @@ -544,7 +546,9 @@ pytools_PYTHON = \
>>         lib/tools/ensure_dirs.py \
>>         lib/tools/node_cleanup.py \
>>         lib/tools/node_daemon_setup.py \
>> -       lib/tools/prepare_node_join.py
>> +       lib/tools/prepare_node_join.py \
>> +       lib/tools/common.py \
>> +       lib/tools/ssh_update.py
>>
>>  utils_PYTHON = \
>>         lib/utils/__init__.py \
>> @@ -1132,7 +1136,8 @@ PYTHON_BOOTSTRAP = \
>>         tools/ensure-dirs \
>>         tools/node-cleanup \
>>         tools/node-daemon-setup \
>> -       tools/prepare-node-join
>> +       tools/prepare-node-join \
>> +       tools/ssh-update
>>
>>  qa_scripts = \
>>         qa/__init__.py \
>> @@ -1304,7 +1309,8 @@ pkglib_python_scripts = \
>>  nodist_pkglib_python_scripts = \
>>         tools/ensure-dirs \
>>         tools/node-daemon-setup \
>> -       tools/prepare-node-join
>> +       tools/prepare-node-join \
>> +       tools/ssh-update
>>
>>  pkglib_python_basenames = \
>>         $(patsubst daemons/%,%,$(patsubst tools/%,%,\
>> @@ -2196,6 +2202,7 @@ tools/burnin: MODULE = ganeti.tools.burnin
>>  tools/ensure-dirs: MODULE = ganeti.tools.ensure_dirs
>>  tools/node-daemon-setup: MODULE = ganeti.tools.node_daemon_setup
>>  tools/prepare-node-join: MODULE = ganeti.tools.prepare_node_join
>> +tools/ssh-update: MODULE = ganeti.tools.ssh_update
>>  tools/node-cleanup: MODULE = ganeti.tools.node_cleanup
>>  $(HS_BUILT_TEST_HELPERS): TESTROLE = $(patsubst test/hs/%,%,$@)
>>
>> diff --git a/lib/pathutils.py b/lib/pathutils.py
>> index 2715504..1cc02e9 100644
>> --- a/lib/pathutils.py
>> +++ b/lib/pathutils.py
>> @@ -55,6 +55,7 @@ IMPORT_EXPORT_DAEMON = _constants.PKGLIBDIR +
>> "/import-export"
>>  KVM_CONSOLE_WRAPPER = _constants.PKGLIBDIR + "/tools/kvm-console-wrapper"
>>  KVM_IFUP = _constants.PKGLIBDIR + "/kvm-ifup"
>>  PREPARE_NODE_JOIN = _constants.PKGLIBDIR + "/prepare-node-join"
>> +SSH_UPDATE = _constants.PKGLIBDIR + "/ssh-update"
>>  NODE_DAEMON_SETUP = _constants.PKGLIBDIR + "/node-daemon-setup"
>>  XEN_CONSOLE_WRAPPER = _constants.PKGLIBDIR + "/tools/xen-console-wrapper"
>>  CFGUPGRADE = _constants.PKGLIBDIR + "/tools/cfgupgrade"
>> diff --git a/lib/tools/common.py b/lib/tools/common.py
>> new file mode 100644
>> index 0000000..de042e4
>> --- /dev/null
>> +++ b/lib/tools/common.py
>> @@ -0,0 +1,100 @@
>> +#
>> +#
>> +
>> +# Copyright (C) 2014 Google Inc.
>> +#
>> +# This program is free software; you can redistribute it and/or modify
>> +# it under the terms of the GNU General Public License as published by
>> +# the Free Software Foundation; either version 2 of the License, or
>> +# (at your option) any later version.
>> +#
>> +# This program is distributed in the hope that it will be useful, but
>> +# WITHOUT ANY WARRANTY; without even the implied warranty of
>> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
>> +# General Public License for more details.
>> +#
>> +# You should have received a copy of the GNU General Public License
>> +# along with this program; if not, write to the Free Software
>> +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
>> +# 02110-1301, USA.
>> +
>> +"""Common functions for tool scripts.
>> +
>> +"""
>> +
>> +import OpenSSL
>> +
>> +from ganeti import constants
>> +from ganeti import errors
>> +from ganeti import utils
>> +from ganeti import serializer
>> +from ganeti import ssconf
>> +
>> +
>> +def VerifyOptions(parser, opts, args):
>> +  """Verifies options and arguments for correctness.
>> +
>> +  """
>> +  if args:
>> +    parser.error("No arguments are expected")
>> +
>> +  return opts
>> +
>> +
>> +def _VerifyCertificate(cert_pem, error_fn,
>> +                       _check_fn=utils.CheckNodeCertificate):
>> +  """Verifies a certificate against the local node daemon certificate.
>> +
>> +  @type cert_pem: string
>> +  @param cert_pem: Certificate in PEM format (no key)
>> +
>> +  """
>> +  try:
>> +    OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
>> +  except OpenSSL.crypto.Error, err:
>> +    pass
>> +  else:
>> +    raise error_fn("No private key may be given")
>> +
>> +  try:
>> +    cert = \
>> +      OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
>> cert_pem)
>> +  except Exception, err:
>> +    raise errors.X509CertError("(stdin)",
>> +                               "Unable to load certificate: %s" % err)
>> +
>> +  _check_fn(cert)
>> +
>> +
>> +def VerifyCertificate(data, error_fn, _verify_fn=_VerifyCertificate):
>> +  """Verifies cluster certificate.
>> +
>> +  @type data: dict
>> +
>> +  """
>> +  cert = data.get(constants.SSHS_NODE_DAEMON_CERTIFICATE)
>> +  if cert:
>> +    _verify_fn(cert, error_fn)
>> +
>> +
>> +def VerifyClusterName(data, error_fn,
>> +                      _verify_fn=ssconf.VerifyClusterName):
>> +  """Verifies cluster name.
>> +
>> +  @type data: dict
>> +
>> +  """
>> +  name = data.get(constants.SSHS_CLUSTER_NAME)
>> +  if name:
>> +    _verify_fn(name)
>> +  else:
>> +    raise error_fn("Cluster name must be specified")
>> +
>> +
>> +def LoadData(raw, data_check):
>> +  """Parses and verifies input data.
>> +
>> +  @rtype: dict
>> +
>> +  """
>> +  return serializer.LoadAndVerifyJson(raw, data_check)
>> diff --git a/lib/tools/prepare_node_join.py
>> b/lib/tools/prepare_node_join.py
>> index 7d96c6a..ed5a227 100644
>> --- a/lib/tools/prepare_node_join.py
>> +++ b/lib/tools/prepare_node_join.py
>> @@ -27,17 +27,15 @@ import os.path
>>  import optparse
>>  import sys
>>  import logging
>> -import OpenSSL
>>
>>  from ganeti import cli
>>  from ganeti import constants
>>  from ganeti import errors
>>  from ganeti import pathutils
>>  from ganeti import utils
>> -from ganeti import serializer
>>  from ganeti import ht
>>  from ganeti import ssh
>> -from ganeti import ssconf
>> +from ganeti.tools import common
>>
>>
>>  _SSH_KEY_LIST_ITEM = \
>> @@ -82,65 +80,7 @@ def ParseOptions():
>>
>>    (opts, args) = parser.parse_args()
>>
>> -  return VerifyOptions(parser, opts, args)
>> -
>> -
>> -def VerifyOptions(parser, opts, args):
>> -  """Verifies options and arguments for correctness.
>> -
>> -  """
>> -  if args:
>> -    parser.error("No arguments are expected")
>> -
>> -  return opts
>> -
>> -
>> -def _VerifyCertificate(cert_pem, _check_fn=utils.CheckNodeCertificate):
>> -  """Verifies a certificate against the local node daemon certificate.
>> -
>> -  @type cert_pem: string
>> -  @param cert_pem: Certificate in PEM format (no key)
>> -
>> -  """
>> -  try:
>> -    OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
>> -  except OpenSSL.crypto.Error, err:
>> -    pass
>> -  else:
>> -    raise JoinError("No private key may be given")
>> -
>> -  try:
>> -    cert = \
>> -      OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
>> cert_pem)
>> -  except Exception, err:
>> -    raise errors.X509CertError("(stdin)",
>> -                               "Unable to load certificate: %s" % err)
>> -
>> -  _check_fn(cert)
>> -
>> -
>> -def VerifyCertificate(data, _verify_fn=_VerifyCertificate):
>> -  """Verifies cluster certificate.
>> -
>> -  @type data: dict
>> -
>> -  """
>> -  cert = data.get(constants.SSHS_NODE_DAEMON_CERTIFICATE)
>> -  if cert:
>> -    _verify_fn(cert)
>> -
>> -
>> -def VerifyClusterName(data, _verify_fn=ssconf.VerifyClusterName):
>> -  """Verifies cluster name.
>> -
>> -  @type data: dict
>> -
>> -  """
>> -  name = data.get(constants.SSHS_CLUSTER_NAME)
>> -  if name:
>> -    _verify_fn(name)
>> -  else:
>> -    raise JoinError("Cluster name must be specified")
>> +  return common.VerifyOptions(parser, opts, args)
>>
>>
>>  def _UpdateKeyFiles(keys, dry_run, keyfiles):
>> @@ -241,15 +181,6 @@ def UpdateSshRoot(data, dry_run, _homedir_fn=None):
>>          ssh.AddAuthorizedKeys(auth_keys_file, all_authorized_keys)
>>
>>
>> -def LoadData(raw):
>> -  """Parses and verifies input data.
>> -
>> -  @rtype: dict
>> -
>> -  """
>> -  return serializer.LoadAndVerifyJson(raw, _DATA_CHECK)
>> -
>> -
>>  def Main():
>>    """Main routine.
>>
>> @@ -259,11 +190,11 @@ def Main():
>>    utils.SetupToolLogging(opts.debug, opts.verbose)
>>
>>    try:
>> -    data = LoadData(sys.stdin.read())
>> +    data = common.LoadData(sys.stdin.read(), _DATA_CHECK)
>>
>>      # Check if input data is correct
>> -    VerifyClusterName(data)
>> -    VerifyCertificate(data)
>> +    common.VerifyClusterName(data, JoinError)
>> +    common.VerifyCertificate(data, JoinError)
>>
>>      # Update SSH files
>>      UpdateSshDaemon(data, opts.dry_run)
>> diff --git a/lib/tools/ssh_update.py b/lib/tools/ssh_update.py
>> new file mode 100644
>> index 0000000..db0f189
>> --- /dev/null
>> +++ b/lib/tools/ssh_update.py
>> @@ -0,0 +1,154 @@
>> +#
>> +#
>> +
>> +# Copyright (C) 2014 Google Inc.
>> +#
>> +# This program is free software; you can redistribute it and/or modify
>> +# it under the terms of the GNU General Public License as published by
>> +# the Free Software Foundation; either version 2 of the License, or
>> +# (at your option) any later version.
>> +#
>> +# This program is distributed in the hope that it will be useful, but
>> +# WITHOUT ANY WARRANTY; without even the implied warranty of
>> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
>> +# General Public License for more details.
>> +#
>> +# You should have received a copy of the GNU General Public License
>> +# along with this program; if not, write to the Free Software
>> +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
>> +# 02110-1301, USA.
>> +
>> +"""Script to update a node's SSH key files.
>> +
>> +This script is used to update the node's 'authorized_keys' and
>> +'ganeti_pub_key' files. It will be called via SSH from the master
>> +node.
>> +
>> +"""
>> +
>> +import os
>> +import os.path
>> +import optparse
>> +import sys
>> +import logging
>> +
>> +from ganeti import cli
>> +from ganeti import constants
>> +from ganeti import errors
>> +from ganeti import utils
>> +from ganeti import ht
>> +from ganeti import ssh
>> +from ganeti import pathutils
>> +from ganeti.tools import common
>> +
>> +
>> +_DATA_CHECK = ht.TStrictDict(False, True, {
>> +  constants.SSHS_CLUSTER_NAME: ht.TNonEmptyString,
>> +  constants.SSHS_NODE_DAEMON_CERTIFICATE: ht.TNonEmptyString,
>> +  constants.SSHS_SSH_PUBLIC_KEYS:
>> +    ht.TDictOf(ht.TNonEmptyString, ht.TListOf(ht.TNonEmptyString)),
>> +  constants.SSHS_SSH_AUTHORIZED_KEYS:
>> +    ht.TDictOf(ht.TNonEmptyString, ht.TListOf(ht.TNonEmptyString)),
>> +  })
>> +
>> +
>> +class SshUpdateError(errors.GenericError):
>> +  """Local class for reporting errors.
>> +
>> +  """
>> +
>> +
>> +def ParseOptions():
>> +  """Parses the options passed to the program.
>> +
>> +  @return: Options and arguments
>> +
>> +  """
>> +  program = os.path.basename(sys.argv[0])
>> +
>> +  parser = optparse.OptionParser(
>> +    usage="%prog [--dry-run] [--verbose] [--debug]", prog=program)
>> +  parser.add_option(cli.DEBUG_OPT)
>> +  parser.add_option(cli.VERBOSE_OPT)
>> +  parser.add_option(cli.DRY_RUN_OPT)
>> +
>> +  (opts, args) = parser.parse_args()
>> +
>> +  return common.VerifyOptions(parser, opts, args)
>> +
>> +
>> +def UpdateAuthorizedKeys(data, dry_run, _homedir_fn=None):
>> +  """Updates root's C{authorized_keys} file.
>> +
>> +  @type data: dict
>> +  @param data: Input data
>> +  @type dry_run: boolean
>> +  @param dry_run: Whether to perform a dry run
>> +
>> +  """
>> +  authorized_keys = data.get(constants.SSHS_SSH_AUTHORIZED_KEYS)
>> +
>> +  if authorized_keys:
>> +    (auth_keys_file, _) = \
>> +      ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=True,
>> +                          _homedir_fn=_homedir_fn)
>> +
>> +    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
>> +      if not os.path.exists(auth_keys_file):
>> +        utils.WriteFile(auth_keys_file, mode=0600, data="")
>> +      ssh.AddAuthorizedKeys(auth_keys_file, all_authorized_keys)
>> +
>> +
>> +def UpdatePubKeyFile(data, dry_run, key_file=pathutils.SSH_PUB_KEYS):
>> +  """Updates the file of public SSH keys.
>> +
>> +  @type data: dict
>> +  @param data: Input data
>> +  @type dry_run: boolean
>> +  @param dry_run: Whether to perform a dry run
>> +
>> +  """
>> +  public_keys = data.get(constants.SSHS_SSH_PUBLIC_KEYS)
>> +  if not public_keys:
>> +    logging.info("No public keys received. Not modifying"
>> +                 " the public key file at all.")
>> +    return
>> +  if dry_run:
>> +    logging.info("This is a dry run, not modifying %s", key_file)
>>
>
> Probably 'else:' is missing here.
>

This is fixed in the scope of an extension of this method in a later patch
anyway. As this is a rather minor thing, I leave it like it is here to
avoid too much rebase mess.


>
>
>> +  ssh.OverridePubKeyFile(public_keys, key_file=key_file)
>> +
>> +
>> +def Main():
>> +  """Main routine.
>> +
>> +  """
>> +  opts = ParseOptions()
>> +
>> +  utils.SetupToolLogging(opts.debug, opts.verbose)
>> +
>> +  try:
>> +    data = common.LoadData(sys.stdin.read(), _DATA_CHECK)
>> +
>> +    # Check if input data is correct
>> +    common.VerifyClusterName(data, SshUpdateError)
>> +    common.VerifyCertificate(data, SshUpdateError)
>> +
>> +    # Update SSH files
>> +    UpdateAuthorizedKeys(data, opts.dry_run)
>> +    UpdatePubKeyFile(data, opts.dry_run)
>> +
>> +    logging.info("Setup finished successfully")
>> +  except Exception, err: # pylint: disable=W0703
>> +    logging.debug("Caught unhandled exception", exc_info=True)
>> +
>> +    (retcode, message) = cli.FormatError(err)
>> +    logging.error(message)
>> +
>> +    return retcode
>> +  else:
>> +    return constants.EXIT_SUCCESS
>> diff --git a/test/py/ganeti.tools.prepare_node_join_unittest.py
>> b/test/py/ganeti.tools.prepare_node_join_unittest.py
>> index fe4ff26..ac30f90 100755
>> --- a/test/py/ganeti.tools.prepare_node_join_unittest.py
>> +++ b/test/py/ganeti.tools.prepare_node_join_unittest.py
>> @@ -25,7 +25,6 @@ import unittest
>>  import shutil
>>  import tempfile
>>  import os.path
>> -import OpenSSL
>>
>>  from ganeti import errors
>>  from ganeti import constants
>> @@ -34,30 +33,31 @@ from ganeti import pathutils
>>  from ganeti import compat
>>  from ganeti import utils
>>  from ganeti.tools import prepare_node_join
>> +from ganeti.tools import common
>>
>>  import testutils
>>
>>
>>  _JoinError = prepare_node_join.JoinError
>> -
>> +_DATA_CHECK = prepare_node_join._DATA_CHECK
>>
>>  class TestLoadData(unittest.TestCase):
>>    def testNoJson(self):
>> -    self.assertRaises(errors.ParseError, prepare_node_join.LoadData, "")
>> -    self.assertRaises(errors.ParseError, prepare_node_join.LoadData, "}")
>> +    self.assertRaises(errors.ParseError, common.LoadData, "",
>> _DATA_CHECK)
>> +    self.assertRaises(errors.ParseError, common.LoadData, "}",
>> _DATA_CHECK)
>>
>>    def testInvalidDataStructure(self):
>>      raw = serializer.DumpJson({
>>        "some other thing": False,
>>        })
>> -    self.assertRaises(errors.ParseError, prepare_node_join.LoadData, raw)
>> +    self.assertRaises(errors.ParseError, common.LoadData, raw,
>> _DATA_CHECK)
>>
>>      raw = serializer.DumpJson([])
>> -    self.assertRaises(errors.ParseError, prepare_node_join.LoadData, raw)
>> +    self.assertRaises(errors.ParseError, common.LoadData, raw,
>> _DATA_CHECK)
>>
>>    def testEmptyDict(self):
>>      raw = serializer.DumpJson({})
>> -    self.assertEqual(prepare_node_join.LoadData(raw), {})
>> +    self.assertEqual(common.LoadData(raw, _DATA_CHECK), {})
>>
>>    def testValidData(self):
>>      key_list = [[constants.SSHK_DSA, "private foo", "public bar"]]
>> @@ -69,7 +69,7 @@ class TestLoadData(unittest.TestCase):
>>          {"nodeuuid01234": ["foo"],
>>           "nodeuuid56789": ["bar"]}}
>>      raw = serializer.DumpJson(data_dict)
>> -    self.assertEqual(prepare_node_join.LoadData(raw), data_dict)
>> +    self.assertEqual(common.LoadData(raw, _DATA_CHECK), data_dict)
>>
>>
>>  class TestVerifyCertificate(testutils.GanetiTestCase):
>> @@ -82,20 +82,21 @@ class TestVerifyCertificate(testutils.GanetiTestCase):
>>      shutil.rmtree(self.tmpdir)
>>
>>    def testNoCert(self):
>> -    prepare_node_join.VerifyCertificate({}, _verify_fn=NotImplemented)
>> +    common.VerifyCertificate({}, error_fn=prepare_node_join.JoinError,
>> +                             _verify_fn=NotImplemented)
>>
>>    def testGivenPrivateKey(self):
>>      cert_filename = testutils.TestDataFilename("cert2.pem")
>>      cert_pem = utils.ReadFile(cert_filename)
>>
>> -    self.assertRaises(_JoinError, prepare_node_join._VerifyCertificate,
>> -                      cert_pem, _check_fn=NotImplemented)
>> +    self.assertRaises(_JoinError, common._VerifyCertificate,
>> +                      cert_pem, _JoinError, _check_fn=NotImplemented)
>>
>>    def testInvalidCertificate(self):
>>      self.assertRaises(errors.X509CertError,
>> -                      prepare_node_join._VerifyCertificate,
>> +                      common._VerifyCertificate,
>>                        "Something that's not a certificate",
>> -                      _check_fn=NotImplemented)
>> +                      _JoinError, _check_fn=NotImplemented)
>>
>>    @staticmethod
>>    def _Check(cert):
>> @@ -104,7 +105,8 @@ class TestVerifyCertificate(testutils.GanetiTestCase):
>>    def testSuccessfulCheck(self):
>>      cert_filename = testutils.TestDataFilename("cert1.pem")
>>      cert_pem = utils.ReadFile(cert_filename)
>> -    prepare_node_join._VerifyCertificate(cert_pem, _check_fn=self._Check)
>> +    common._VerifyCertificate(cert_pem, _JoinError,
>> +      _check_fn=self._Check)
>>
>>
>>  class TestVerifyClusterName(unittest.TestCase):
>> @@ -117,8 +119,8 @@ class TestVerifyClusterName(unittest.TestCase):
>>      shutil.rmtree(self.tmpdir)
>>
>>    def testNoName(self):
>> -    self.assertRaises(_JoinError, prepare_node_join.VerifyClusterName,
>> -                      {}, _verify_fn=NotImplemented)
>> +    self.assertRaises(_JoinError, common.VerifyClusterName,
>> +                      {}, _JoinError, _verify_fn=NotImplemented)
>>
>>    @staticmethod
>>    def _FailingVerify(name):
>> @@ -130,8 +132,8 @@ class TestVerifyClusterName(unittest.TestCase):
>>        constants.SSHS_CLUSTER_NAME: "cluster.example.com",
>>        }
>>
>> -    self.assertRaises(errors.GenericError,
>> prepare_node_join.VerifyClusterName,
>> -                      data, _verify_fn=self._FailingVerify)
>> +    self.assertRaises(errors.GenericError, common.VerifyClusterName,
>> +                      data, _JoinError, _verify_fn=self._FailingVerify)
>>
>>
>>  class TestUpdateSshDaemon(unittest.TestCase):
>> diff --git a/test/py/ganeti.tools.ssh_update_unittest.py b/test/py/
>> ganeti.tools.ssh_update_unittest.py
>> new file mode 100755
>> index 0000000..af3205a
>> --- /dev/null
>> +++ b/test/py/ganeti.tools.ssh_update_unittest.py
>> @@ -0,0 +1,123 @@
>> +#!/usr/bin/python
>> +#
>> +
>> +# Copyright (C) 2014 Google Inc.
>> +#
>> +# This program is free software; you can redistribute it and/or modify
>> +# it under the terms of the GNU General Public License as published by
>> +# the Free Software Foundation; either version 2 of the License, or
>> +# (at your option) any later version.
>> +#
>> +# This program is distributed in the hope that it will be useful, but
>> +# WITHOUT ANY WARRANTY; without even the implied warranty of
>> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
>> +# General Public License for more details.
>> +#
>> +# You should have received a copy of the GNU General Public License
>> +# along with this program; if not, write to the Free Software
>> +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
>> +# 02110-1301, USA.
>> +
>> +
>> +"""Script for testing ganeti.tools.ssh_update"""
>> +
>> +import unittest
>> +import shutil
>> +import tempfile
>> +import os.path
>> +
>> +from ganeti import constants
>> +from ganeti import utils
>> +from ganeti.tools import ssh_update
>> +
>> +import testutils
>> +
>> +
>> +_JoinError = ssh_update.SshUpdateError
>> +_DATA_CHECK = ssh_update._DATA_CHECK
>> +
>> +
>> +class TestUpdateAuthorizedKeys(testutils.GanetiTestCase):
>> +  def setUp(self):
>> +    testutils.GanetiTestCase.setUp(self)
>> +    self.tmpdir = tempfile.mkdtemp()
>> +    self.sshdir = utils.PathJoin(self.tmpdir, ".ssh")
>> +
>> +  def tearDown(self):
>> +    unittest.TestCase.tearDown(self)
>> +    shutil.rmtree(self.tmpdir)
>> +
>> +  def _GetHomeDir(self, user):
>> +    self.assertEqual(user, constants.SSH_LOGIN_USER)
>> +    return self.tmpdir
>> +
>> +  def testNoKeys(self):
>> +    data_empty_keys = {
>> +      constants.SSHS_SSH_AUTHORIZED_KEYS: {},
>> +      }
>> +
>> +    for data in [{}, data_empty_keys]:
>> +      for dry_run in [False, True]:
>> +        ssh_update.UpdateAuthorizedKeys(data, dry_run,
>> +                                        _homedir_fn=NotImplemented)
>> +    self.assertEqual(os.listdir(self.tmpdir), [])
>> +
>> +  def testDryRun(self):
>> +    data = {
>> +      constants.SSHS_SSH_AUTHORIZED_KEYS: {
>> +        "node1" : ["key11", "key12", "key13"],
>> +        "node2" : ["key21", "key22"]},
>> +      }
>> +
>> +    ssh_update.UpdateAuthorizedKeys(data, True,
>> +                                    _homedir_fn=self._GetHomeDir)
>> +    self.assertEqual(os.listdir(self.tmpdir), [".ssh"])
>> +    self.assertEqual(os.listdir(self.sshdir), [])
>> +
>> +  def testUpdate(self):
>> +    data = {
>> +      constants.SSHS_SSH_AUTHORIZED_KEYS: {
>> +        "node1": ["key11", "key12"],
>> +        "node2": ["key21"]},
>> +      }
>> +
>> +    ssh_update.UpdateAuthorizedKeys(data, False,
>> +                                    _homedir_fn=self._GetHomeDir)
>> +    self.assertEqual(os.listdir(self.tmpdir), [".ssh"])
>> +    self.assertEqual(sorted(os.listdir(self.sshdir)),
>> +                     sorted(["authorized_keys"]))
>> +    self.assertEqual(utils.ReadFile(utils.PathJoin(self.sshdir,
>> +                                                   "authorized_keys")),
>> +                     "key11\nkey12\nkey21\n")
>> +
>> +
>> +class TestUpdatePubKeyFile(testutils.GanetiTestCase):
>> +  def setUp(self):
>> +    testutils.GanetiTestCase.setUp(self)
>> +
>> +  def testNoKeys(self):
>> +    pub_key_file = self._CreateTempFile()
>> +    data_empty_keys = {
>> +      constants.SSHS_SSH_PUBLIC_KEYS: {},
>> +      }
>> +
>> +    for data in [{}, data_empty_keys]:
>> +      for dry_run in [False, True]:
>> +        ssh_update.UpdatePubKeyFile(data, dry_run,
>> +                                    key_file=pub_key_file)
>> +    self.assertEqual(utils.ReadFile(pub_key_file), "")
>> +
>> +  def testValidKeys(self):
>> +    pub_key_file = self._CreateTempFile()
>> +    data = {
>> +      constants.SSHS_SSH_PUBLIC_KEYS: {
>> +        "node1": ["key11", "key12"],
>> +        "node2": ["key21"]},
>> +      }
>> +    ssh_update.UpdatePubKeyFile(data, False, key_file=pub_key_file)
>> +    self.assertEqual(utils.ReadFile(pub_key_file),
>> +      "node1 key11\nnode1 key12\nnode2 key21\n")
>> +
>> +
>> +if __name__ == "__main__":
>> +  testutils.GanetiTestProgram()
>> --
>> 2.0.0.526.g5318336
>>
>>
> Rest LGTM
>

Thx.



-- 
Helga Velroyen | Software Engineer | hel...@google.com |

Google Germany GmbH
Dienerstr. 12
80331 München

Registergericht und -nummer: Hamburg, HRB 86891
Sitz der Gesellschaft: Hamburg
Geschäftsführer: Graham Law, Christine Elizabeth Flores

Reply via email to