Statistics
| Branch: | Tag: | Revision:

root / tools / setup-ssh @ 3dc66ebc

History | View | Annotate | Download (10 kB)

1
#!/usr/bin/python
2
#
3

    
4
# Copyright (C) 2010 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21
"""Tool to setup the SSH configuration on a remote node.
22

    
23
This is needed before we can join the node into the cluster.
24

    
25
"""
26

    
27
# pylint: disable-msg=C0103
28
# C0103: Invalid name setup-ssh
29

    
30
import getpass
31
import logging
32
import paramiko
33
import os.path
34
import optparse
35
import sys
36

    
37
from ganeti import cli
38
from ganeti import constants
39
from ganeti import errors
40
from ganeti import netutils
41
from ganeti import ssh
42
from ganeti import utils
43

    
44

    
45
class RemoteCommandError(errors.GenericError):
46
  """Exception if remote command was not successful.
47

    
48
  """
49

    
50

    
51
def _RunRemoteCommand(transport, command):
52
  """Invokes and wait for the command over SSH.
53

    
54
  @param transport: The paramiko transport instance
55
  @param command: The command to be executed
56

    
57
  """
58
  chan = transport.open_session()
59
  chan.set_combine_stderr(True)
60
  output_handler = chan.makefile("r")
61
  chan.exec_command(command)
62

    
63
  result = chan.recv_exit_status()
64
  msg = output_handler.read()
65

    
66
  out_msg = "'%s' exited with status code %s, output %r" % (command, result,
67
                                                            msg)
68

    
69
  # If result is -1 (no exit status provided) we assume it was not successful
70
  if result:
71
    raise RemoteCommandError(out_msg)
72

    
73
  if msg:
74
    logging.info(out_msg)
75

    
76

    
77
def _InvokeDaemonUtil(transport, command):
78
  """Invokes daemon-util on the remote side.
79

    
80
  @param transport: The paramiko transport instance
81
  @param command: The daemon-util command to be run
82

    
83
  """
84
  _RunRemoteCommand(transport, "%s %s" % (constants.DAEMON_UTIL, command))
85

    
86

    
87
def _WriteSftpFile(sftp, name, perm, data):
88
  """SFTPs data to a remote file.
89

    
90
  @param sftp: A open paramiko SFTP client
91
  @param name: The remote file name
92
  @param perm: The remote file permission
93
  @param data: The data to write
94

    
95
  """
96
  remote_file = sftp.open(name, "w")
97
  try:
98
    sftp.chmod(name, perm)
99
    remote_file.write(data)
100
  finally:
101
    remote_file.close()
102

    
103

    
104
def SetupSSH(transport):
105
  """Sets the SSH up on the other side.
106

    
107
  @param transport: The paramiko transport instance
108

    
109
  """
110
  priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS)
111
  keyfiles = [
112
    (constants.SSH_HOST_DSA_PRIV, 0600),
113
    (constants.SSH_HOST_DSA_PUB, 0644),
114
    (constants.SSH_HOST_RSA_PRIV, 0600),
115
    (constants.SSH_HOST_RSA_PUB, 0644),
116
    (priv_key, 0600),
117
    (pub_key, 0644),
118
    ]
119

    
120
  sftp = transport.open_sftp_client()
121

    
122
  filemap = dict((name, (utils.ReadFile(name), perm))
123
                 for (name, perm) in keyfiles)
124

    
125
  auth_path = os.path.dirname(auth_keys)
126

    
127
  try:
128
    sftp.mkdir(auth_path, 0700)
129
  except IOError:
130
    # Sadly paramiko doesn't provide errno or similiar
131
    # so we can just assume that the path already exists
132
    logging.info("Path %s seems already to exist on remote node. Ignoring.",
133
                 auth_path)
134

    
135
  for name, (data, perm) in filemap.iteritems():
136
    _WriteSftpFile(sftp, name, perm, data)
137

    
138
  authorized_keys = sftp.open(auth_keys, "a+")
139
  try:
140
    # We don't have to close, as the close happened already in AddAuthorizedKey
141
    utils.AddAuthorizedKey(authorized_keys, filemap[pub_key][0])
142
  finally:
143
    authorized_keys.close()
144

    
145
  _InvokeDaemonUtil(transport, "reload-ssh-keys")
146

    
147

    
148
def SetupNodeDaemon(transport):
149
  """Sets the node daemon up on the other side.
150

    
151
  @param transport: The paramiko transport instance
152

    
153
  """
154
  noded_cert = utils.ReadFile(constants.NODED_CERT_FILE)
155

    
156
  sftp = transport.open_sftp_client()
157
  _WriteSftpFile(sftp, constants.NODED_CERT_FILE, 0400, noded_cert)
158

    
159
  _InvokeDaemonUtil(transport, "start %s" % constants.NODED)
160

    
161

    
162
def ParseOptions():
163
  """Parses options passed to program.
164

    
165
  """
166
  program = os.path.basename(sys.argv[0])
167

    
168
  parser = optparse.OptionParser(usage=("%prog [--debug|--verbose] <node>"
169
                                        " <node...>"), prog=program)
170
  parser.add_option(cli.DEBUG_OPT)
171
  parser.add_option(cli.VERBOSE_OPT)
172
  default_key = ssh.GetUserFiles(constants.GANETI_RUNAS)[0]
173
  parser.add_option(optparse.Option("-f", dest="private_key",
174
                                    default=default_key,
175
                                    help="The private key to (try to) use for"
176
                                    "authentication "))
177
  parser.add_option(optparse.Option("--key-type", dest="key_type",
178
                                    choices=("rsa", "dsa"), default="dsa",
179
                                    help="The private key type (rsa or dsa)"))
180

    
181
  (options, args) = parser.parse_args()
182

    
183
  return (options, args)
184

    
185

    
186
def SetupLogging(options):
187
  """Sets up the logging.
188

    
189
  @param options: Parsed options
190

    
191
  """
192
  fmt = "%(asctime)s: %(threadName)s "
193
  if options.debug or options.verbose:
194
    fmt += "%(levelname)s "
195
  fmt += "%(message)s"
196

    
197
  formatter = logging.Formatter(fmt)
198

    
199
  file_handler = logging.FileHandler(constants.LOG_SETUP_SSH)
200
  stderr_handler = logging.StreamHandler()
201
  stderr_handler.setFormatter(formatter)
202
  file_handler.setFormatter(formatter)
203
  file_handler.setLevel(logging.INFO)
204

    
205
  if options.debug:
206
    stderr_handler.setLevel(logging.DEBUG)
207
  elif options.verbose:
208
    stderr_handler.setLevel(logging.INFO)
209
  else:
210
    stderr_handler.setLevel(logging.WARNING)
211

    
212
  root_logger = logging.getLogger("")
213
  root_logger.setLevel(logging.NOTSET)
214
  root_logger.addHandler(stderr_handler)
215
  root_logger.addHandler(file_handler)
216

    
217
  # This is the paramiko logger instance
218
  paramiko_logger = logging.getLogger("paramiko")
219
  paramiko_logger.addHandler(file_handler)
220
  # We don't want to debug Paramiko, so filter anything below warning
221
  paramiko_logger.setLevel(logging.WARNING)
222

    
223

    
224
def LoadPrivateKeys(options):
225
  """Load the list of available private keys
226

    
227
  It loads the standard ssh key from disk and then tries to connect to
228
  the ssh agent too.
229

    
230
  @rtype: list
231
  @return: a list of C{paramiko.PKey}
232

    
233
  """
234
  if options.key_type == "rsa":
235
    pkclass = paramiko.RSAKey
236
  elif options.key_type == "dsa":
237
    pkclass = paramiko.DSSKey
238
  else:
239
    logging.critical("Unknown key type %s selected (choose either rsa or dsa)",
240
                     options.key_type)
241
    sys.exit(1)
242

    
243
  try:
244
    private_key = pkclass.from_private_key_file(options.private_key)
245
  except (paramiko.SSHException, EnvironmentError), err:
246
    logging.critical("Can't load private key %s: %s", options.private_key, err)
247
    sys.exit(1)
248

    
249
  try:
250
    agent = paramiko.Agent()
251
    agent_keys = agent.get_keys()
252
  except paramiko.SSHException, err:
253
    # this will only be seen when the agent is broken/uses invalid
254
    # protocol; for non-existing agent, get_keys() will just return an
255
    # empty tuple
256
    logging.warning("Can't connect to the ssh agent: %s; skipping its use",
257
                    err)
258
    agent_keys = []
259

    
260
  return [private_key] + list(agent_keys)
261

    
262

    
263
def LoginViaKeys(transport, username, keys):
264
  """Try to login on the given transport via a list of keys.
265

    
266
  @param transport: the transport to use
267
  @param username: the username to login as
268
  @type keys: list
269
  @param keys: list of C{paramiko.PKey} to use for authentication
270
  @rtype: boolean
271
  @return: True or False depending on whether the login was
272
      successfull or not
273

    
274
  """
275
  for private_key in keys:
276
    try:
277
      transport.auth_publickey(username, private_key)
278
      fpr = ":".join("%02x" % ord(i) for i in private_key.get_fingerprint())
279
      if isinstance(private_key, paramiko.AgentKey):
280
        logging.debug("Authentication via the ssh-agent key %s", fpr)
281
      else:
282
        logging.debug("Authenticated via public key %s", fpr)
283
      return True
284
    except paramiko.SSHException:
285
      continue
286
  else:
287
    # all keys exhausted
288
    return False
289

    
290

    
291
def main():
292
  """Main routine.
293

    
294
  """
295
  (options, args) = ParseOptions()
296

    
297
  SetupLogging(options)
298

    
299
  all_keys = LoadPrivateKeys(options)
300

    
301
  passwd = None
302
  username = constants.GANETI_RUNAS
303
  ssh_port = netutils.GetDaemonPort("ssh")
304

    
305
  # Below, we need to join() the transport objects, as otherwise the
306
  # following happens:
307
  # - the main thread finishes
308
  # - the atexit functions run (in the main thread), and cause the
309
  #   logging file to be closed
310
  # - a tiny bit later, the transport thread is finally ending, and
311
  #   wants to log one more message, which fails as the file is closed
312
  #   now
313

    
314
  for host in args:
315
    transport = paramiko.Transport((host, ssh_port))
316
    transport.start_client()
317
    try:
318
      if LoginViaKeys(transport, username, all_keys):
319
        logging.info("Authenticated to %s via public key", host)
320
      else:
321
        logging.warning("Authentication to %s via public key failed, trying"
322
                        " password", host)
323
        if passwd is None:
324
          passwd = getpass.getpass(prompt="%s password:" % username)
325
        transport.auth_password(username=username, password=passwd)
326
        logging.info("Authenticated to %s via password", host)
327
    except paramiko.SSHException, err:
328
      logging.error("Connection or authentication failed to host %s: %s",
329
                    host, err)
330
      transport.close()
331
      # this is needed for compatibility with older Paramiko or Python
332
      # versions
333
      transport.join()
334
      continue
335
    try:
336
      try:
337
        SetupSSH(transport)
338
        SetupNodeDaemon(transport)
339
      except errors.GenericError, err:
340
        logging.error("While doing setup on host %s an error occured: %s",
341
                      host, err)
342
    finally:
343
      transport.close()
344
      # this is needed for compatibility with older Paramiko or Python
345
      # versions
346
      transport.join()
347

    
348

    
349
if __name__ == "__main__":
350
  main()