Statistics
| Branch: | Tag: | Revision:

root / tools / setup-ssh @ 7eda951b

History | View | Annotate | Download (14.3 kB)

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