Statistics
| Branch: | Tag: | Revision:

root / tools / setup-ssh @ 7a6a27af

History | View | Annotate | Download (8.5 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.INFO)
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 main():
225
  """Main routine.
226

    
227
  """
228
  (options, args) = ParseOptions()
229

    
230
  SetupLogging(options)
231

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

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

    
247
  passwd = None
248
  username = constants.GANETI_RUNAS
249
  ssh_port = netutils.GetDaemonPort("ssh")
250

    
251
  # Below, we need to join() the transport objects, as otherwise the
252
  # following happens:
253
  # - the main thread finishes
254
  # - the atexit functions run (in the main thread), and cause the
255
  #   logging file to be closed
256
  # - a tiny bit later, the transport thread is finally ending, and
257
  #   wants to log one more message, which fails as the file is closed
258
  #   now
259

    
260
  for host in args:
261
    transport = paramiko.Transport((host, ssh_port))
262
    transport.start_client()
263
    try:
264
      try:
265
        transport.auth_publickey(username, private_key)
266
        logging.info("Authenticated to %s via public key", host)
267
      except paramiko.SSHException:
268
        logging.warning("Authentication to %s via public key failed, trying"
269
                        " password", host)
270
        if passwd is None:
271
          passwd = getpass.getpass(prompt="%s password:" % username)
272
        transport.auth_password(username=username, password=passwd)
273
        logging.info("Authenticated to %s via password", host)
274
    except paramiko.SSHException, err:
275
      logging.error("Connection or authentication failed to host %s: %s",
276
                    host, err)
277
      transport.close()
278
      # this is needed for compatibility with older Paramiko or Python
279
      # versions
280
      transport.join()
281
      continue
282
    try:
283
      try:
284
        SetupSSH(transport)
285
        SetupNodeDaemon(transport)
286
      except errors.GenericError, err:
287
        logging.error("While doing setup on host %s an error occured: %s",
288
                      host, err)
289
    finally:
290
      transport.close()
291
      # this is needed for compatibility with older Paramiko or Python
292
      # versions
293
      transport.join()
294

    
295

    
296
if __name__ == "__main__":
297
  main()