Statistics
| Branch: | Tag: | Revision:

root / tools / setup-ssh @ 7bff16bd

History | View | Annotate | Download (6.9 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. Ignore.",
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

    
173
  (options, args) = parser.parse_args()
174

    
175
  return (options, args)
176

    
177

    
178
def SetupLogging(options):
179
  """Sets up the logging.
180

    
181
  @param options: Parsed options
182

    
183
  """
184
  fmt = "%(asctime)s: %(threadName)s "
185
  if options.debug or options.verbose:
186
    fmt += "%(levelname)s "
187
  fmt += "%(message)s"
188

    
189
  formatter = logging.Formatter(fmt)
190

    
191
  file_handler = logging.FileHandler(constants.LOG_SETUP_SSH)
192
  stderr_handler = logging.StreamHandler()
193
  stderr_handler.setFormatter(formatter)
194
  file_handler.setFormatter(formatter)
195
  file_handler.setLevel(logging.DEBUG)
196

    
197
  if options.debug:
198
    stderr_handler.setLevel(logging.NOTSET)
199
  elif options.verbose:
200
    stderr_handler.setLevel(logging.INFO)
201
  else:
202
    stderr_handler.setLevel(logging.ERROR)
203

    
204
  root_logger = logging.getLogger("")
205
  root_logger.setLevel(logging.NOTSET)
206
  root_logger.addHandler(stderr_handler)
207
  root_logger.addHandler(file_handler)
208

    
209
  # This is the paramiko logger instance
210
  paramiko_logger = logging.getLogger("paramiko")
211
  paramiko_logger.addHandler(file_handler)
212

    
213

    
214
def main():
215
  """Main routine.
216

    
217
  """
218
  (options, args) = ParseOptions()
219

    
220
  SetupLogging(options)
221

    
222
  passwd = getpass.getpass(prompt="%s password:" % constants.GANETI_RUNAS)
223
  ssh_port = netutils.GetDaemonPort("ssh")
224

    
225
  # Below, we need to join() the transport objects, as otherwise the
226
  # following happens:
227
  # - the main thread finishes
228
  # - the atexit functions run (in the main thread), and cause the
229
  #   logging file to be closed
230
  # - a tiny bit later, the transport thread is finally ending, and
231
  #   wants to log one more message, which fails as the file is closed
232
  #   now
233

    
234
  for host in args:
235
    transport = paramiko.Transport((host, ssh_port))
236
    try:
237
      transport.connect(username=constants.GANETI_RUNAS, password=passwd)
238
    except Exception, err:
239
      logging.error("Connection or authentication failed to host %s: %s",
240
                    host, err)
241
      transport.close()
242
      # this is needed for compatibility with older Paramiko or Python
243
      # versions
244
      transport.join()
245
      continue
246
    try:
247
      try:
248
        SetupSSH(transport)
249
        SetupNodeDaemon(transport)
250
      except errors.GenericError, err:
251
        logging.error("While doing setup on host %s an error occured: %s",
252
                      host, err)
253
    finally:
254
      transport.close()
255
      # this is needed for compatibility with older Paramiko or Python
256
      # versions
257
      transport.join()
258

    
259

    
260
if __name__ == "__main__":
261
  main()