Statistics
| Branch: | Tag: | Revision:

root / tools / setup-ssh @ 82989b8d

History | View | Annotate | Download (14.6 kB)

1 05cd934d René Nussbaumer
#!/usr/bin/python
2 05cd934d René Nussbaumer
#
3 05cd934d René Nussbaumer
4 f1bebf4c Iustin Pop
# Copyright (C) 2010, 2012 Google Inc.
5 05cd934d René Nussbaumer
#
6 05cd934d René Nussbaumer
# This program is free software; you can redistribute it and/or modify
7 05cd934d René Nussbaumer
# it under the terms of the GNU General Public License as published by
8 05cd934d René Nussbaumer
# the Free Software Foundation; either version 2 of the License, or
9 05cd934d René Nussbaumer
# (at your option) any later version.
10 05cd934d René Nussbaumer
#
11 05cd934d René Nussbaumer
# This program is distributed in the hope that it will be useful, but
12 05cd934d René Nussbaumer
# WITHOUT ANY WARRANTY; without even the implied warranty of
13 05cd934d René Nussbaumer
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 05cd934d René Nussbaumer
# General Public License for more details.
15 05cd934d René Nussbaumer
#
16 05cd934d René Nussbaumer
# You should have received a copy of the GNU General Public License
17 05cd934d René Nussbaumer
# along with this program; if not, write to the Free Software
18 05cd934d René Nussbaumer
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 05cd934d René Nussbaumer
# 02110-1301, USA.
20 05cd934d René Nussbaumer
21 05cd934d René Nussbaumer
"""Tool to setup the SSH configuration on a remote node.
22 05cd934d René Nussbaumer
23 05cd934d René Nussbaumer
This is needed before we can join the node into the cluster.
24 05cd934d René Nussbaumer
25 05cd934d René Nussbaumer
"""
26 05cd934d René Nussbaumer
27 b459a848 Andrea Spadaccini
# pylint: disable=C0103
28 c9a4a662 Manuel Franceschini
# C0103: Invalid name setup-ssh
29 c9a4a662 Manuel Franceschini
30 05cd934d René Nussbaumer
import getpass
31 05cd934d René Nussbaumer
import logging
32 05cd934d René Nussbaumer
import os.path
33 05cd934d René Nussbaumer
import optparse
34 05cd934d René Nussbaumer
import sys
35 05cd934d René Nussbaumer
36 3ec10f0d Iustin Pop
# workaround paramiko warnings
37 3ec10f0d Iustin Pop
# FIXME: use 'with warnings.catch_warnings' once we drop Python 2.4
38 3ec10f0d Iustin Pop
import warnings
39 3ec10f0d Iustin Pop
warnings.simplefilter("ignore")
40 3ec10f0d Iustin Pop
import paramiko
41 3ec10f0d Iustin Pop
warnings.resetwarnings()
42 3ec10f0d Iustin Pop
43 05cd934d René Nussbaumer
from ganeti import cli
44 05cd934d René Nussbaumer
from ganeti import constants
45 05cd934d René Nussbaumer
from ganeti import errors
46 05cd934d René Nussbaumer
from ganeti import netutils
47 d3b18b8e René Nussbaumer
from ganeti import ssconf
48 05cd934d René Nussbaumer
from ganeti import ssh
49 05cd934d René Nussbaumer
from ganeti import utils
50 05cd934d René Nussbaumer
51 05cd934d René Nussbaumer
52 05cd934d René Nussbaumer
class RemoteCommandError(errors.GenericError):
53 05cd934d René Nussbaumer
  """Exception if remote command was not successful.
54 05cd934d René Nussbaumer
55 05cd934d René Nussbaumer
  """
56 05cd934d René Nussbaumer
57 05cd934d René Nussbaumer
58 d3b18b8e René Nussbaumer
class JoinCheckError(errors.GenericError):
59 d3b18b8e René Nussbaumer
  """Exception raised if join check fails.
60 d3b18b8e René Nussbaumer
61 d3b18b8e René Nussbaumer
  """
62 d3b18b8e René Nussbaumer
63 d3b18b8e René Nussbaumer
64 c4da9eaf René Nussbaumer
class HostKeyVerificationError(errors.GenericError):
65 c4da9eaf René Nussbaumer
  """Exception if host key do not match.
66 c4da9eaf René Nussbaumer
67 c4da9eaf René Nussbaumer
  """
68 c4da9eaf René Nussbaumer
69 c4da9eaf René Nussbaumer
70 c4da9eaf René Nussbaumer
class AuthError(errors.GenericError):
71 c4da9eaf René Nussbaumer
  """Exception for authentication errors to hosts.
72 c4da9eaf René Nussbaumer
73 c4da9eaf René Nussbaumer
  """
74 c4da9eaf René Nussbaumer
75 c4da9eaf René Nussbaumer
76 d3b18b8e René Nussbaumer
def _CheckJoin(transport):
77 d3b18b8e René Nussbaumer
  """Checks if a join is safe or dangerous.
78 d3b18b8e René Nussbaumer
79 d3b18b8e René Nussbaumer
  Note: This function relies on the fact, that all
80 d3b18b8e René Nussbaumer
  hosts have the same configuration at compile time of
81 d3b18b8e René Nussbaumer
  Ganeti. So that the constants do not mismatch.
82 d3b18b8e René Nussbaumer
83 d3b18b8e René Nussbaumer
  @param transport: The paramiko transport instance
84 d3b18b8e René Nussbaumer
  @return: True if the join is safe; False otherwise
85 d3b18b8e René Nussbaumer
86 d3b18b8e René Nussbaumer
  """
87 d3b18b8e René Nussbaumer
  sftp = transport.open_sftp_client()
88 d3b18b8e René Nussbaumer
  ss = ssconf.SimpleStore()
89 d3b18b8e René Nussbaumer
  ss_cluster_name_path = ss.KeyToFilename(constants.SS_CLUSTER_NAME)
90 d3b18b8e René Nussbaumer
91 c4da9eaf René Nussbaumer
  cluster_files = [
92 c4da9eaf René Nussbaumer
    (constants.NODED_CERT_FILE, utils.ReadFile(constants.NODED_CERT_FILE)),
93 c4da9eaf René Nussbaumer
    (ss_cluster_name_path, utils.ReadFile(ss_cluster_name_path)),
94 c4da9eaf René Nussbaumer
    ]
95 d3b18b8e René Nussbaumer
96 c4da9eaf René Nussbaumer
  for (filename, local_content) in cluster_files:
97 c4da9eaf René Nussbaumer
    try:
98 c4da9eaf René Nussbaumer
      remote_content = _ReadSftpFile(sftp, filename)
99 c4da9eaf René Nussbaumer
    except IOError, err:
100 c4da9eaf René Nussbaumer
      # Assume file does not exist. Paramiko's error reporting is lacking.
101 c4da9eaf René Nussbaumer
      logging.debug("Failed to read %s: %s", filename, err)
102 c4da9eaf René Nussbaumer
      continue
103 d3b18b8e René Nussbaumer
104 c4da9eaf René Nussbaumer
    if remote_content != local_content:
105 c4da9eaf René Nussbaumer
      logging.error("File %s doesn't match local version", filename)
106 c4da9eaf René Nussbaumer
      return False
107 d3b18b8e René Nussbaumer
108 c4da9eaf René Nussbaumer
  return True
109 d3b18b8e René Nussbaumer
110 d3b18b8e René Nussbaumer
111 05cd934d René Nussbaumer
def _RunRemoteCommand(transport, command):
112 05cd934d René Nussbaumer
  """Invokes and wait for the command over SSH.
113 05cd934d René Nussbaumer
114 05cd934d René Nussbaumer
  @param transport: The paramiko transport instance
115 05cd934d René Nussbaumer
  @param command: The command to be executed
116 05cd934d René Nussbaumer
117 05cd934d René Nussbaumer
  """
118 05cd934d René Nussbaumer
  chan = transport.open_session()
119 05cd934d René Nussbaumer
  chan.set_combine_stderr(True)
120 05cd934d René Nussbaumer
  output_handler = chan.makefile("r")
121 05cd934d René Nussbaumer
  chan.exec_command(command)
122 05cd934d René Nussbaumer
123 05cd934d René Nussbaumer
  result = chan.recv_exit_status()
124 05cd934d René Nussbaumer
  msg = output_handler.read()
125 05cd934d René Nussbaumer
126 05cd934d René Nussbaumer
  out_msg = "'%s' exited with status code %s, output %r" % (command, result,
127 05cd934d René Nussbaumer
                                                            msg)
128 05cd934d René Nussbaumer
129 05cd934d René Nussbaumer
  # If result is -1 (no exit status provided) we assume it was not successful
130 05cd934d René Nussbaumer
  if result:
131 05cd934d René Nussbaumer
    raise RemoteCommandError(out_msg)
132 05cd934d René Nussbaumer
133 05cd934d René Nussbaumer
  if msg:
134 05cd934d René Nussbaumer
    logging.info(out_msg)
135 05cd934d René Nussbaumer
136 05cd934d René Nussbaumer
137 05cd934d René Nussbaumer
def _InvokeDaemonUtil(transport, command):
138 05cd934d René Nussbaumer
  """Invokes daemon-util on the remote side.
139 05cd934d René Nussbaumer
140 05cd934d René Nussbaumer
  @param transport: The paramiko transport instance
141 05cd934d René Nussbaumer
  @param command: The daemon-util command to be run
142 05cd934d René Nussbaumer
143 05cd934d René Nussbaumer
  """
144 05cd934d René Nussbaumer
  _RunRemoteCommand(transport, "%s %s" % (constants.DAEMON_UTIL, command))
145 05cd934d René Nussbaumer
146 05cd934d René Nussbaumer
147 d3b18b8e René Nussbaumer
def _ReadSftpFile(sftp, filename):
148 d3b18b8e René Nussbaumer
  """Reads a file over sftp.
149 d3b18b8e René Nussbaumer
150 d3b18b8e René Nussbaumer
  @param sftp: An open paramiko SFTP client
151 d3b18b8e René Nussbaumer
  @param filename: The filename of the file to read
152 d3b18b8e René Nussbaumer
  @return: The content of the file
153 d3b18b8e René Nussbaumer
154 d3b18b8e René Nussbaumer
  """
155 d3b18b8e René Nussbaumer
  remote_file = sftp.open(filename, "r")
156 d3b18b8e René Nussbaumer
  try:
157 d3b18b8e René Nussbaumer
    return remote_file.read()
158 d3b18b8e René Nussbaumer
  finally:
159 d3b18b8e René Nussbaumer
    remote_file.close()
160 d3b18b8e René Nussbaumer
161 d3b18b8e René Nussbaumer
162 05cd934d René Nussbaumer
def _WriteSftpFile(sftp, name, perm, data):
163 05cd934d René Nussbaumer
  """SFTPs data to a remote file.
164 05cd934d René Nussbaumer
165 05cd934d René Nussbaumer
  @param sftp: A open paramiko SFTP client
166 05cd934d René Nussbaumer
  @param name: The remote file name
167 05cd934d René Nussbaumer
  @param perm: The remote file permission
168 05cd934d René Nussbaumer
  @param data: The data to write
169 05cd934d René Nussbaumer
170 05cd934d René Nussbaumer
  """
171 05cd934d René Nussbaumer
  remote_file = sftp.open(name, "w")
172 05cd934d René Nussbaumer
  try:
173 05cd934d René Nussbaumer
    sftp.chmod(name, perm)
174 05cd934d René Nussbaumer
    remote_file.write(data)
175 05cd934d René Nussbaumer
  finally:
176 05cd934d René Nussbaumer
    remote_file.close()
177 05cd934d René Nussbaumer
178 05cd934d René Nussbaumer
179 05cd934d René Nussbaumer
def SetupSSH(transport):
180 05cd934d René Nussbaumer
  """Sets the SSH up on the other side.
181 05cd934d René Nussbaumer
182 05cd934d René Nussbaumer
  @param transport: The paramiko transport instance
183 05cd934d René Nussbaumer
184 05cd934d René Nussbaumer
  """
185 05cd934d René Nussbaumer
  priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS)
186 05cd934d René Nussbaumer
  keyfiles = [
187 05cd934d René Nussbaumer
    (constants.SSH_HOST_DSA_PRIV, 0600),
188 05cd934d René Nussbaumer
    (constants.SSH_HOST_DSA_PUB, 0644),
189 05cd934d René Nussbaumer
    (constants.SSH_HOST_RSA_PRIV, 0600),
190 05cd934d René Nussbaumer
    (constants.SSH_HOST_RSA_PUB, 0644),
191 05cd934d René Nussbaumer
    (priv_key, 0600),
192 05cd934d René Nussbaumer
    (pub_key, 0644),
193 05cd934d René Nussbaumer
    ]
194 05cd934d René Nussbaumer
195 05cd934d René Nussbaumer
  sftp = transport.open_sftp_client()
196 05cd934d René Nussbaumer
197 05cd934d René Nussbaumer
  filemap = dict((name, (utils.ReadFile(name), perm))
198 05cd934d René Nussbaumer
                 for (name, perm) in keyfiles)
199 05cd934d René Nussbaumer
200 05cd934d René Nussbaumer
  auth_path = os.path.dirname(auth_keys)
201 05cd934d René Nussbaumer
202 05cd934d René Nussbaumer
  try:
203 05cd934d René Nussbaumer
    sftp.mkdir(auth_path, 0700)
204 c4da9eaf René Nussbaumer
  except IOError, err:
205 05cd934d René Nussbaumer
    # Sadly paramiko doesn't provide errno or similiar
206 05cd934d René Nussbaumer
    # so we can just assume that the path already exists
207 c4da9eaf René Nussbaumer
    logging.info("Assuming directory %s on remote node exists: %s",
208 c4da9eaf René Nussbaumer
                 auth_path, err)
209 05cd934d René Nussbaumer
210 05cd934d René Nussbaumer
  for name, (data, perm) in filemap.iteritems():
211 05cd934d René Nussbaumer
    _WriteSftpFile(sftp, name, perm, data)
212 05cd934d René Nussbaumer
213 05cd934d René Nussbaumer
  authorized_keys = sftp.open(auth_keys, "a+")
214 05cd934d René Nussbaumer
  try:
215 634a9a35 Iustin Pop
    # Due to the way SFTPFile and BufferedFile are implemented,
216 634a9a35 Iustin Pop
    # opening in a+ mode and then issuing a read(), readline() or
217 634a9a35 Iustin Pop
    # iterating over the file (which uses read() internally) will see
218 634a9a35 Iustin Pop
    # an empty file, since the paramiko internal file position and the
219 634a9a35 Iustin Pop
    # OS-level file-position are desynchronized; therefore, we issue
220 634a9a35 Iustin Pop
    # an explicit seek to resynchronize these; writes should (note
221 634a9a35 Iustin Pop
    # should) still go to the right place
222 634a9a35 Iustin Pop
    authorized_keys.seek(0, 0)
223 05cd934d René Nussbaumer
    # We don't have to close, as the close happened already in AddAuthorizedKey
224 05cd934d René Nussbaumer
    utils.AddAuthorizedKey(authorized_keys, filemap[pub_key][0])
225 05cd934d René Nussbaumer
  finally:
226 05cd934d René Nussbaumer
    authorized_keys.close()
227 05cd934d René Nussbaumer
228 05cd934d René Nussbaumer
  _InvokeDaemonUtil(transport, "reload-ssh-keys")
229 05cd934d René Nussbaumer
230 05cd934d René Nussbaumer
231 05cd934d René Nussbaumer
def ParseOptions():
232 05cd934d René Nussbaumer
  """Parses options passed to program.
233 05cd934d René Nussbaumer
234 05cd934d René Nussbaumer
  """
235 05cd934d René Nussbaumer
  program = os.path.basename(sys.argv[0])
236 05cd934d René Nussbaumer
237 d3b18b8e René Nussbaumer
  parser = optparse.OptionParser(usage=("%prog [--debug|--verbose] [--force]"
238 d3b18b8e René Nussbaumer
                                        " <node> <node...>"), prog=program)
239 05cd934d René Nussbaumer
  parser.add_option(cli.DEBUG_OPT)
240 05cd934d René Nussbaumer
  parser.add_option(cli.VERBOSE_OPT)
241 310a8944 René Nussbaumer
  parser.add_option(cli.NOSSH_KEYCHECK_OPT)
242 7a6a27af Iustin Pop
  default_key = ssh.GetUserFiles(constants.GANETI_RUNAS)[0]
243 7a6a27af Iustin Pop
  parser.add_option(optparse.Option("-f", dest="private_key",
244 7a6a27af Iustin Pop
                                    default=default_key,
245 7a6a27af Iustin Pop
                                    help="The private key to (try to) use for"
246 7a6a27af Iustin Pop
                                    "authentication "))
247 7a6a27af Iustin Pop
  parser.add_option(optparse.Option("--key-type", dest="key_type",
248 7a6a27af Iustin Pop
                                    choices=("rsa", "dsa"), default="dsa",
249 7a6a27af Iustin Pop
                                    help="The private key type (rsa or dsa)"))
250 d3b18b8e René Nussbaumer
  parser.add_option(optparse.Option("-j", "--force-join", dest="force_join",
251 d3b18b8e René Nussbaumer
                                    action="store_true", default=False,
252 d3b18b8e René Nussbaumer
                                    help="Force the join of the host"))
253 05cd934d René Nussbaumer
254 05cd934d René Nussbaumer
  (options, args) = parser.parse_args()
255 05cd934d René Nussbaumer
256 f1bebf4c Iustin Pop
  if not args:
257 f1bebf4c Iustin Pop
    parser.print_help()
258 f1bebf4c Iustin Pop
    sys.exit(constants.EXIT_FAILURE)
259 f1bebf4c Iustin Pop
260 05cd934d René Nussbaumer
  return (options, args)
261 05cd934d René Nussbaumer
262 05cd934d René Nussbaumer
263 05cd934d René Nussbaumer
def SetupLogging(options):
264 05cd934d René Nussbaumer
  """Sets up the logging.
265 05cd934d René Nussbaumer
266 05cd934d René Nussbaumer
  @param options: Parsed options
267 05cd934d René Nussbaumer
268 05cd934d René Nussbaumer
  """
269 05cd934d René Nussbaumer
  fmt = "%(asctime)s: %(threadName)s "
270 05cd934d René Nussbaumer
  if options.debug or options.verbose:
271 05cd934d René Nussbaumer
    fmt += "%(levelname)s "
272 05cd934d René Nussbaumer
  fmt += "%(message)s"
273 05cd934d René Nussbaumer
274 05cd934d René Nussbaumer
  formatter = logging.Formatter(fmt)
275 05cd934d René Nussbaumer
276 05cd934d René Nussbaumer
  file_handler = logging.FileHandler(constants.LOG_SETUP_SSH)
277 05cd934d René Nussbaumer
  stderr_handler = logging.StreamHandler()
278 05cd934d René Nussbaumer
  stderr_handler.setFormatter(formatter)
279 05cd934d René Nussbaumer
  file_handler.setFormatter(formatter)
280 5c654e95 Iustin Pop
  file_handler.setLevel(logging.INFO)
281 05cd934d René Nussbaumer
282 05cd934d René Nussbaumer
  if options.debug:
283 5c654e95 Iustin Pop
    stderr_handler.setLevel(logging.DEBUG)
284 05cd934d René Nussbaumer
  elif options.verbose:
285 05cd934d René Nussbaumer
    stderr_handler.setLevel(logging.INFO)
286 05cd934d René Nussbaumer
  else:
287 5c654e95 Iustin Pop
    stderr_handler.setLevel(logging.WARNING)
288 05cd934d René Nussbaumer
289 05cd934d René Nussbaumer
  root_logger = logging.getLogger("")
290 3dc66ebc Iustin Pop
  root_logger.setLevel(logging.NOTSET)
291 05cd934d René Nussbaumer
  root_logger.addHandler(stderr_handler)
292 05cd934d René Nussbaumer
  root_logger.addHandler(file_handler)
293 8647a52c Iustin Pop
294 8647a52c Iustin Pop
  # This is the paramiko logger instance
295 8647a52c Iustin Pop
  paramiko_logger = logging.getLogger("paramiko")
296 05cd934d René Nussbaumer
  paramiko_logger.addHandler(file_handler)
297 5c654e95 Iustin Pop
  # We don't want to debug Paramiko, so filter anything below warning
298 5c654e95 Iustin Pop
  paramiko_logger.setLevel(logging.WARNING)
299 05cd934d René Nussbaumer
300 05cd934d René Nussbaumer
301 3dc66ebc Iustin Pop
def LoadPrivateKeys(options):
302 697a3d61 Manuel Franceschini
  """Load the list of available private keys.
303 05cd934d René Nussbaumer
304 3dc66ebc Iustin Pop
  It loads the standard ssh key from disk and then tries to connect to
305 3dc66ebc Iustin Pop
  the ssh agent too.
306 05cd934d René Nussbaumer
307 3dc66ebc Iustin Pop
  @rtype: list
308 3dc66ebc Iustin Pop
  @return: a list of C{paramiko.PKey}
309 05cd934d René Nussbaumer
310 3dc66ebc Iustin Pop
  """
311 7a6a27af Iustin Pop
  if options.key_type == "rsa":
312 7a6a27af Iustin Pop
    pkclass = paramiko.RSAKey
313 7a6a27af Iustin Pop
  elif options.key_type == "dsa":
314 7a6a27af Iustin Pop
    pkclass = paramiko.DSSKey
315 7a6a27af Iustin Pop
  else:
316 7a6a27af Iustin Pop
    logging.critical("Unknown key type %s selected (choose either rsa or dsa)",
317 7a6a27af Iustin Pop
                     options.key_type)
318 7a6a27af Iustin Pop
    sys.exit(1)
319 7a6a27af Iustin Pop
320 7a6a27af Iustin Pop
  try:
321 7a6a27af Iustin Pop
    private_key = pkclass.from_private_key_file(options.private_key)
322 7a6a27af Iustin Pop
  except (paramiko.SSHException, EnvironmentError), err:
323 7a6a27af Iustin Pop
    logging.critical("Can't load private key %s: %s", options.private_key, err)
324 7a6a27af Iustin Pop
    sys.exit(1)
325 7a6a27af Iustin Pop
326 3dc66ebc Iustin Pop
  try:
327 3dc66ebc Iustin Pop
    agent = paramiko.Agent()
328 3dc66ebc Iustin Pop
    agent_keys = agent.get_keys()
329 3dc66ebc Iustin Pop
  except paramiko.SSHException, err:
330 3dc66ebc Iustin Pop
    # this will only be seen when the agent is broken/uses invalid
331 3dc66ebc Iustin Pop
    # protocol; for non-existing agent, get_keys() will just return an
332 3dc66ebc Iustin Pop
    # empty tuple
333 3dc66ebc Iustin Pop
    logging.warning("Can't connect to the ssh agent: %s; skipping its use",
334 3dc66ebc Iustin Pop
                    err)
335 3dc66ebc Iustin Pop
    agent_keys = []
336 3dc66ebc Iustin Pop
337 3dc66ebc Iustin Pop
  return [private_key] + list(agent_keys)
338 3dc66ebc Iustin Pop
339 3dc66ebc Iustin Pop
340 c4da9eaf René Nussbaumer
def _FormatFingerprint(fpr):
341 c4da9eaf René Nussbaumer
  """Formats a paramiko.PKey.get_fingerprint() human readable.
342 c4da9eaf René Nussbaumer
343 c4da9eaf René Nussbaumer
  @param fpr: The fingerprint to be formatted
344 c4da9eaf René Nussbaumer
  @return: A human readable fingerprint
345 c4da9eaf René Nussbaumer
346 c4da9eaf René Nussbaumer
  """
347 c4da9eaf René Nussbaumer
  return ssh.FormatParamikoFingerprint(paramiko.util.hexify(fpr))
348 c4da9eaf René Nussbaumer
349 c4da9eaf René Nussbaumer
350 3dc66ebc Iustin Pop
def LoginViaKeys(transport, username, keys):
351 3dc66ebc Iustin Pop
  """Try to login on the given transport via a list of keys.
352 3dc66ebc Iustin Pop
353 3dc66ebc Iustin Pop
  @param transport: the transport to use
354 3dc66ebc Iustin Pop
  @param username: the username to login as
355 3dc66ebc Iustin Pop
  @type keys: list
356 3dc66ebc Iustin Pop
  @param keys: list of C{paramiko.PKey} to use for authentication
357 3dc66ebc Iustin Pop
  @rtype: boolean
358 3dc66ebc Iustin Pop
  @return: True or False depending on whether the login was
359 3dc66ebc Iustin Pop
      successfull or not
360 3dc66ebc Iustin Pop
361 3dc66ebc Iustin Pop
  """
362 3dc66ebc Iustin Pop
  for private_key in keys:
363 3dc66ebc Iustin Pop
    try:
364 3dc66ebc Iustin Pop
      transport.auth_publickey(username, private_key)
365 c4da9eaf René Nussbaumer
      fpr = _FormatFingerprint(private_key.get_fingerprint())
366 3dc66ebc Iustin Pop
      if isinstance(private_key, paramiko.AgentKey):
367 3dc66ebc Iustin Pop
        logging.debug("Authentication via the ssh-agent key %s", fpr)
368 3dc66ebc Iustin Pop
      else:
369 3dc66ebc Iustin Pop
        logging.debug("Authenticated via public key %s", fpr)
370 3dc66ebc Iustin Pop
      return True
371 3dc66ebc Iustin Pop
    except paramiko.SSHException:
372 3dc66ebc Iustin Pop
      continue
373 3dc66ebc Iustin Pop
  else:
374 3dc66ebc Iustin Pop
    # all keys exhausted
375 3dc66ebc Iustin Pop
    return False
376 3dc66ebc Iustin Pop
377 3dc66ebc Iustin Pop
378 310a8944 René Nussbaumer
def LoadKnownHosts():
379 697a3d61 Manuel Franceschini
  """Load the known hosts.
380 310a8944 René Nussbaumer
381 697a3d61 Manuel Franceschini
  @return: paramiko.util.load_host_keys dict
382 310a8944 René Nussbaumer
383 310a8944 René Nussbaumer
  """
384 310a8944 René Nussbaumer
  homedir = utils.GetHomeDir(constants.GANETI_RUNAS)
385 310a8944 René Nussbaumer
  known_hosts = os.path.join(homedir, ".ssh", "known_hosts")
386 310a8944 René Nussbaumer
387 310a8944 René Nussbaumer
  try:
388 310a8944 René Nussbaumer
    return paramiko.util.load_host_keys(known_hosts)
389 310a8944 René Nussbaumer
  except EnvironmentError:
390 c4da9eaf René Nussbaumer
    # We didn't find the path, silently ignore and return an empty dict
391 310a8944 René Nussbaumer
    return {}
392 310a8944 René Nussbaumer
393 310a8944 René Nussbaumer
394 c4da9eaf René Nussbaumer
def _VerifyServerKey(transport, host, host_keys):
395 c4da9eaf René Nussbaumer
  """Verify the server keys.
396 c4da9eaf René Nussbaumer
397 c4da9eaf René Nussbaumer
  @param transport: A paramiko.transport instance
398 c4da9eaf René Nussbaumer
  @param host: Name of the host we verify
399 c4da9eaf René Nussbaumer
  @param host_keys: Loaded host keys
400 c4da9eaf René Nussbaumer
  @raises HostkeyVerificationError: When the host identify couldn't be verified
401 c4da9eaf René Nussbaumer
402 c4da9eaf René Nussbaumer
  """
403 c4da9eaf René Nussbaumer
404 c4da9eaf René Nussbaumer
  server_key = transport.get_remote_server_key()
405 c4da9eaf René Nussbaumer
  keytype = server_key.get_name()
406 c4da9eaf René Nussbaumer
407 c4da9eaf René Nussbaumer
  our_server_key = host_keys.get(host, {}).get(keytype, None)
408 c4da9eaf René Nussbaumer
  if not our_server_key:
409 c4da9eaf René Nussbaumer
    hexified_key = _FormatFingerprint(server_key.get_fingerprint())
410 c4da9eaf René Nussbaumer
    msg = ("Unable to verify hostkey of host %s: %s. Do you want to accept"
411 c4da9eaf René Nussbaumer
           " it?" % (host, hexified_key))
412 c4da9eaf René Nussbaumer
413 c4da9eaf René Nussbaumer
    if cli.AskUser(msg):
414 c4da9eaf René Nussbaumer
      our_server_key = server_key
415 c4da9eaf René Nussbaumer
416 c4da9eaf René Nussbaumer
  if our_server_key != server_key:
417 c4da9eaf René Nussbaumer
    raise HostKeyVerificationError("Unable to verify host identity")
418 c4da9eaf René Nussbaumer
419 c4da9eaf René Nussbaumer
420 3dc66ebc Iustin Pop
def main():
421 3dc66ebc Iustin Pop
  """Main routine.
422 3dc66ebc Iustin Pop
423 3dc66ebc Iustin Pop
  """
424 3dc66ebc Iustin Pop
  (options, args) = ParseOptions()
425 3dc66ebc Iustin Pop
426 3dc66ebc Iustin Pop
  SetupLogging(options)
427 3dc66ebc Iustin Pop
428 3dc66ebc Iustin Pop
  all_keys = LoadPrivateKeys(options)
429 3dc66ebc Iustin Pop
430 7a6a27af Iustin Pop
  passwd = None
431 7a6a27af Iustin Pop
  username = constants.GANETI_RUNAS
432 7bff16bd Iustin Pop
  ssh_port = netutils.GetDaemonPort("ssh")
433 310a8944 René Nussbaumer
  host_keys = LoadKnownHosts()
434 05cd934d René Nussbaumer
435 8647a52c Iustin Pop
  # Below, we need to join() the transport objects, as otherwise the
436 8647a52c Iustin Pop
  # following happens:
437 8647a52c Iustin Pop
  # - the main thread finishes
438 8647a52c Iustin Pop
  # - the atexit functions run (in the main thread), and cause the
439 8647a52c Iustin Pop
  #   logging file to be closed
440 8647a52c Iustin Pop
  # - a tiny bit later, the transport thread is finally ending, and
441 8647a52c Iustin Pop
  #   wants to log one more message, which fails as the file is closed
442 8647a52c Iustin Pop
  #   now
443 8647a52c Iustin Pop
444 c4da9eaf René Nussbaumer
  success = True
445 c4da9eaf René Nussbaumer
446 05cd934d René Nussbaumer
  for host in args:
447 c4da9eaf René Nussbaumer
    logging.info("Configuring %s", host)
448 310a8944 René Nussbaumer
449 c4da9eaf René Nussbaumer
    transport = paramiko.Transport((host, ssh_port))
450 05cd934d René Nussbaumer
    try:
451 05cd934d René Nussbaumer
      try:
452 c4da9eaf René Nussbaumer
        transport.start_client()
453 c4da9eaf René Nussbaumer
454 c4da9eaf René Nussbaumer
        if options.ssh_key_check:
455 c4da9eaf René Nussbaumer
          _VerifyServerKey(transport, host, host_keys)
456 c4da9eaf René Nussbaumer
457 c4da9eaf René Nussbaumer
        try:
458 c4da9eaf René Nussbaumer
          if LoginViaKeys(transport, username, all_keys):
459 c4da9eaf René Nussbaumer
            logging.info("Authenticated to %s via public key", host)
460 d3b18b8e René Nussbaumer
          else:
461 c4da9eaf René Nussbaumer
            if all_keys:
462 c4da9eaf René Nussbaumer
              logging.warning("Authentication to %s via public key failed,"
463 c4da9eaf René Nussbaumer
                              " trying password", host)
464 c4da9eaf René Nussbaumer
            if passwd is None:
465 c4da9eaf René Nussbaumer
              passwd = getpass.getpass(prompt="%s password:" % username)
466 c4da9eaf René Nussbaumer
            transport.auth_password(username=username, password=passwd)
467 c4da9eaf René Nussbaumer
            logging.info("Authenticated to %s via password", host)
468 c4da9eaf René Nussbaumer
        except paramiko.SSHException, err:
469 c4da9eaf René Nussbaumer
          raise AuthError("Auth error TODO" % err)
470 c4da9eaf René Nussbaumer
471 c4da9eaf René Nussbaumer
        if not _CheckJoin(transport):
472 c4da9eaf René Nussbaumer
          if not options.force_join:
473 e389d95b René Nussbaumer
            raise JoinCheckError(("Host %s failed join check; Please verify"
474 e389d95b René Nussbaumer
                                  " that the host was not previously joined"
475 e389d95b René Nussbaumer
                                  " to another cluster and use --force-join"
476 e389d95b René Nussbaumer
                                  " to continue") % host)
477 c4da9eaf René Nussbaumer
478 c4da9eaf René Nussbaumer
          logging.warning("Host %s failed join check, forced to continue",
479 c4da9eaf René Nussbaumer
                          host)
480 c4da9eaf René Nussbaumer
481 05cd934d René Nussbaumer
        SetupSSH(transport)
482 c4da9eaf René Nussbaumer
        logging.info("%s successfully configured", host)
483 c4da9eaf René Nussbaumer
      finally:
484 c4da9eaf René Nussbaumer
        transport.close()
485 c4da9eaf René Nussbaumer
        # this is needed for compatibility with older Paramiko or Python
486 c4da9eaf René Nussbaumer
        # versions
487 c4da9eaf René Nussbaumer
        transport.join()
488 c4da9eaf René Nussbaumer
    except AuthError, err:
489 c4da9eaf René Nussbaumer
      logging.error("Authentication error: %s", err)
490 c4da9eaf René Nussbaumer
      success = False
491 c4da9eaf René Nussbaumer
      break
492 c4da9eaf René Nussbaumer
    except HostKeyVerificationError, err:
493 c4da9eaf René Nussbaumer
      logging.error("Host key verification error: %s", err)
494 c4da9eaf René Nussbaumer
      success = False
495 c4da9eaf René Nussbaumer
    except Exception, err:
496 c4da9eaf René Nussbaumer
      logging.exception("During setup of %s: %s", host, err)
497 c4da9eaf René Nussbaumer
      success = False
498 c4da9eaf René Nussbaumer
499 c4da9eaf René Nussbaumer
  if success:
500 c4da9eaf René Nussbaumer
    sys.exit(constants.EXIT_SUCCESS)
501 c4da9eaf René Nussbaumer
502 c4da9eaf René Nussbaumer
  sys.exit(constants.EXIT_FAILURE)
503 5b27346a René Nussbaumer
504 05cd934d René Nussbaumer
505 05cd934d René Nussbaumer
if __name__ == "__main__":
506 05cd934d René Nussbaumer
  main()