Statistics
| Branch: | Tag: | Revision:

root / tools / setup-ssh @ 3dc66ebc

History | View | Annotate | Download (10 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 c9a4a662 Manuel Franceschini
# pylint: disable-msg=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 05cd934d René Nussbaumer
from ganeti import ssh
42 05cd934d René Nussbaumer
from ganeti import utils
43 05cd934d René Nussbaumer
44 05cd934d René Nussbaumer
45 05cd934d René Nussbaumer
class RemoteCommandError(errors.GenericError):
46 05cd934d René Nussbaumer
  """Exception if remote command was not successful.
47 05cd934d René Nussbaumer
48 05cd934d René Nussbaumer
  """
49 05cd934d René Nussbaumer
50 05cd934d René Nussbaumer
51 05cd934d René Nussbaumer
def _RunRemoteCommand(transport, command):
52 05cd934d René Nussbaumer
  """Invokes and wait for the command over SSH.
53 05cd934d René Nussbaumer
54 05cd934d René Nussbaumer
  @param transport: The paramiko transport instance
55 05cd934d René Nussbaumer
  @param command: The command to be executed
56 05cd934d René Nussbaumer
57 05cd934d René Nussbaumer
  """
58 05cd934d René Nussbaumer
  chan = transport.open_session()
59 05cd934d René Nussbaumer
  chan.set_combine_stderr(True)
60 05cd934d René Nussbaumer
  output_handler = chan.makefile("r")
61 05cd934d René Nussbaumer
  chan.exec_command(command)
62 05cd934d René Nussbaumer
63 05cd934d René Nussbaumer
  result = chan.recv_exit_status()
64 05cd934d René Nussbaumer
  msg = output_handler.read()
65 05cd934d René Nussbaumer
66 05cd934d René Nussbaumer
  out_msg = "'%s' exited with status code %s, output %r" % (command, result,
67 05cd934d René Nussbaumer
                                                            msg)
68 05cd934d René Nussbaumer
69 05cd934d René Nussbaumer
  # If result is -1 (no exit status provided) we assume it was not successful
70 05cd934d René Nussbaumer
  if result:
71 05cd934d René Nussbaumer
    raise RemoteCommandError(out_msg)
72 05cd934d René Nussbaumer
73 05cd934d René Nussbaumer
  if msg:
74 05cd934d René Nussbaumer
    logging.info(out_msg)
75 05cd934d René Nussbaumer
76 05cd934d René Nussbaumer
77 05cd934d René Nussbaumer
def _InvokeDaemonUtil(transport, command):
78 05cd934d René Nussbaumer
  """Invokes daemon-util on the remote side.
79 05cd934d René Nussbaumer
80 05cd934d René Nussbaumer
  @param transport: The paramiko transport instance
81 05cd934d René Nussbaumer
  @param command: The daemon-util command to be run
82 05cd934d René Nussbaumer
83 05cd934d René Nussbaumer
  """
84 05cd934d René Nussbaumer
  _RunRemoteCommand(transport, "%s %s" % (constants.DAEMON_UTIL, command))
85 05cd934d René Nussbaumer
86 05cd934d René Nussbaumer
87 05cd934d René Nussbaumer
def _WriteSftpFile(sftp, name, perm, data):
88 05cd934d René Nussbaumer
  """SFTPs data to a remote file.
89 05cd934d René Nussbaumer
90 05cd934d René Nussbaumer
  @param sftp: A open paramiko SFTP client
91 05cd934d René Nussbaumer
  @param name: The remote file name
92 05cd934d René Nussbaumer
  @param perm: The remote file permission
93 05cd934d René Nussbaumer
  @param data: The data to write
94 05cd934d René Nussbaumer
95 05cd934d René Nussbaumer
  """
96 05cd934d René Nussbaumer
  remote_file = sftp.open(name, "w")
97 05cd934d René Nussbaumer
  try:
98 05cd934d René Nussbaumer
    sftp.chmod(name, perm)
99 05cd934d René Nussbaumer
    remote_file.write(data)
100 05cd934d René Nussbaumer
  finally:
101 05cd934d René Nussbaumer
    remote_file.close()
102 05cd934d René Nussbaumer
103 05cd934d René Nussbaumer
104 05cd934d René Nussbaumer
def SetupSSH(transport):
105 05cd934d René Nussbaumer
  """Sets the SSH up on the other side.
106 05cd934d René Nussbaumer
107 05cd934d René Nussbaumer
  @param transport: The paramiko transport instance
108 05cd934d René Nussbaumer
109 05cd934d René Nussbaumer
  """
110 05cd934d René Nussbaumer
  priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS)
111 05cd934d René Nussbaumer
  keyfiles = [
112 05cd934d René Nussbaumer
    (constants.SSH_HOST_DSA_PRIV, 0600),
113 05cd934d René Nussbaumer
    (constants.SSH_HOST_DSA_PUB, 0644),
114 05cd934d René Nussbaumer
    (constants.SSH_HOST_RSA_PRIV, 0600),
115 05cd934d René Nussbaumer
    (constants.SSH_HOST_RSA_PUB, 0644),
116 05cd934d René Nussbaumer
    (priv_key, 0600),
117 05cd934d René Nussbaumer
    (pub_key, 0644),
118 05cd934d René Nussbaumer
    ]
119 05cd934d René Nussbaumer
120 05cd934d René Nussbaumer
  sftp = transport.open_sftp_client()
121 05cd934d René Nussbaumer
122 05cd934d René Nussbaumer
  filemap = dict((name, (utils.ReadFile(name), perm))
123 05cd934d René Nussbaumer
                 for (name, perm) in keyfiles)
124 05cd934d René Nussbaumer
125 05cd934d René Nussbaumer
  auth_path = os.path.dirname(auth_keys)
126 05cd934d René Nussbaumer
127 05cd934d René Nussbaumer
  try:
128 05cd934d René Nussbaumer
    sftp.mkdir(auth_path, 0700)
129 05cd934d René Nussbaumer
  except IOError:
130 05cd934d René Nussbaumer
    # Sadly paramiko doesn't provide errno or similiar
131 05cd934d René Nussbaumer
    # so we can just assume that the path already exists
132 5c654e95 Iustin Pop
    logging.info("Path %s seems already to exist on remote node. Ignoring.",
133 05cd934d René Nussbaumer
                 auth_path)
134 05cd934d René Nussbaumer
135 05cd934d René Nussbaumer
  for name, (data, perm) in filemap.iteritems():
136 05cd934d René Nussbaumer
    _WriteSftpFile(sftp, name, perm, data)
137 05cd934d René Nussbaumer
138 05cd934d René Nussbaumer
  authorized_keys = sftp.open(auth_keys, "a+")
139 05cd934d René Nussbaumer
  try:
140 05cd934d René Nussbaumer
    # We don't have to close, as the close happened already in AddAuthorizedKey
141 05cd934d René Nussbaumer
    utils.AddAuthorizedKey(authorized_keys, filemap[pub_key][0])
142 05cd934d René Nussbaumer
  finally:
143 05cd934d René Nussbaumer
    authorized_keys.close()
144 05cd934d René Nussbaumer
145 05cd934d René Nussbaumer
  _InvokeDaemonUtil(transport, "reload-ssh-keys")
146 05cd934d René Nussbaumer
147 05cd934d René Nussbaumer
148 05cd934d René Nussbaumer
def SetupNodeDaemon(transport):
149 05cd934d René Nussbaumer
  """Sets the node daemon up on the other side.
150 05cd934d René Nussbaumer
151 05cd934d René Nussbaumer
  @param transport: The paramiko transport instance
152 05cd934d René Nussbaumer
153 05cd934d René Nussbaumer
  """
154 05cd934d René Nussbaumer
  noded_cert = utils.ReadFile(constants.NODED_CERT_FILE)
155 05cd934d René Nussbaumer
156 05cd934d René Nussbaumer
  sftp = transport.open_sftp_client()
157 05cd934d René Nussbaumer
  _WriteSftpFile(sftp, constants.NODED_CERT_FILE, 0400, noded_cert)
158 05cd934d René Nussbaumer
159 05cd934d René Nussbaumer
  _InvokeDaemonUtil(transport, "start %s" % constants.NODED)
160 05cd934d René Nussbaumer
161 05cd934d René Nussbaumer
162 05cd934d René Nussbaumer
def ParseOptions():
163 05cd934d René Nussbaumer
  """Parses options passed to program.
164 05cd934d René Nussbaumer
165 05cd934d René Nussbaumer
  """
166 05cd934d René Nussbaumer
  program = os.path.basename(sys.argv[0])
167 05cd934d René Nussbaumer
168 05cd934d René Nussbaumer
  parser = optparse.OptionParser(usage=("%prog [--debug|--verbose] <node>"
169 05cd934d René Nussbaumer
                                        " <node...>"), prog=program)
170 05cd934d René Nussbaumer
  parser.add_option(cli.DEBUG_OPT)
171 05cd934d René Nussbaumer
  parser.add_option(cli.VERBOSE_OPT)
172 7a6a27af Iustin Pop
  default_key = ssh.GetUserFiles(constants.GANETI_RUNAS)[0]
173 7a6a27af Iustin Pop
  parser.add_option(optparse.Option("-f", dest="private_key",
174 7a6a27af Iustin Pop
                                    default=default_key,
175 7a6a27af Iustin Pop
                                    help="The private key to (try to) use for"
176 7a6a27af Iustin Pop
                                    "authentication "))
177 7a6a27af Iustin Pop
  parser.add_option(optparse.Option("--key-type", dest="key_type",
178 7a6a27af Iustin Pop
                                    choices=("rsa", "dsa"), default="dsa",
179 7a6a27af Iustin Pop
                                    help="The private key type (rsa or dsa)"))
180 05cd934d René Nussbaumer
181 05cd934d René Nussbaumer
  (options, args) = parser.parse_args()
182 05cd934d René Nussbaumer
183 05cd934d René Nussbaumer
  return (options, args)
184 05cd934d René Nussbaumer
185 05cd934d René Nussbaumer
186 05cd934d René Nussbaumer
def SetupLogging(options):
187 05cd934d René Nussbaumer
  """Sets up the logging.
188 05cd934d René Nussbaumer
189 05cd934d René Nussbaumer
  @param options: Parsed options
190 05cd934d René Nussbaumer
191 05cd934d René Nussbaumer
  """
192 05cd934d René Nussbaumer
  fmt = "%(asctime)s: %(threadName)s "
193 05cd934d René Nussbaumer
  if options.debug or options.verbose:
194 05cd934d René Nussbaumer
    fmt += "%(levelname)s "
195 05cd934d René Nussbaumer
  fmt += "%(message)s"
196 05cd934d René Nussbaumer
197 05cd934d René Nussbaumer
  formatter = logging.Formatter(fmt)
198 05cd934d René Nussbaumer
199 05cd934d René Nussbaumer
  file_handler = logging.FileHandler(constants.LOG_SETUP_SSH)
200 05cd934d René Nussbaumer
  stderr_handler = logging.StreamHandler()
201 05cd934d René Nussbaumer
  stderr_handler.setFormatter(formatter)
202 05cd934d René Nussbaumer
  file_handler.setFormatter(formatter)
203 5c654e95 Iustin Pop
  file_handler.setLevel(logging.INFO)
204 05cd934d René Nussbaumer
205 05cd934d René Nussbaumer
  if options.debug:
206 5c654e95 Iustin Pop
    stderr_handler.setLevel(logging.DEBUG)
207 05cd934d René Nussbaumer
  elif options.verbose:
208 05cd934d René Nussbaumer
    stderr_handler.setLevel(logging.INFO)
209 05cd934d René Nussbaumer
  else:
210 5c654e95 Iustin Pop
    stderr_handler.setLevel(logging.WARNING)
211 05cd934d René Nussbaumer
212 05cd934d René Nussbaumer
  root_logger = logging.getLogger("")
213 3dc66ebc Iustin Pop
  root_logger.setLevel(logging.NOTSET)
214 05cd934d René Nussbaumer
  root_logger.addHandler(stderr_handler)
215 05cd934d René Nussbaumer
  root_logger.addHandler(file_handler)
216 8647a52c Iustin Pop
217 8647a52c Iustin Pop
  # This is the paramiko logger instance
218 8647a52c Iustin Pop
  paramiko_logger = logging.getLogger("paramiko")
219 05cd934d René Nussbaumer
  paramiko_logger.addHandler(file_handler)
220 5c654e95 Iustin Pop
  # We don't want to debug Paramiko, so filter anything below warning
221 5c654e95 Iustin Pop
  paramiko_logger.setLevel(logging.WARNING)
222 05cd934d René Nussbaumer
223 05cd934d René Nussbaumer
224 3dc66ebc Iustin Pop
def LoadPrivateKeys(options):
225 3dc66ebc Iustin Pop
  """Load the list of available private keys
226 05cd934d René Nussbaumer
227 3dc66ebc Iustin Pop
  It loads the standard ssh key from disk and then tries to connect to
228 3dc66ebc Iustin Pop
  the ssh agent too.
229 05cd934d René Nussbaumer
230 3dc66ebc Iustin Pop
  @rtype: list
231 3dc66ebc Iustin Pop
  @return: a list of C{paramiko.PKey}
232 05cd934d René Nussbaumer
233 3dc66ebc Iustin Pop
  """
234 7a6a27af Iustin Pop
  if options.key_type == "rsa":
235 7a6a27af Iustin Pop
    pkclass = paramiko.RSAKey
236 7a6a27af Iustin Pop
  elif options.key_type == "dsa":
237 7a6a27af Iustin Pop
    pkclass = paramiko.DSSKey
238 7a6a27af Iustin Pop
  else:
239 7a6a27af Iustin Pop
    logging.critical("Unknown key type %s selected (choose either rsa or dsa)",
240 7a6a27af Iustin Pop
                     options.key_type)
241 7a6a27af Iustin Pop
    sys.exit(1)
242 7a6a27af Iustin Pop
243 7a6a27af Iustin Pop
  try:
244 7a6a27af Iustin Pop
    private_key = pkclass.from_private_key_file(options.private_key)
245 7a6a27af Iustin Pop
  except (paramiko.SSHException, EnvironmentError), err:
246 7a6a27af Iustin Pop
    logging.critical("Can't load private key %s: %s", options.private_key, err)
247 7a6a27af Iustin Pop
    sys.exit(1)
248 7a6a27af Iustin Pop
249 3dc66ebc Iustin Pop
  try:
250 3dc66ebc Iustin Pop
    agent = paramiko.Agent()
251 3dc66ebc Iustin Pop
    agent_keys = agent.get_keys()
252 3dc66ebc Iustin Pop
  except paramiko.SSHException, err:
253 3dc66ebc Iustin Pop
    # this will only be seen when the agent is broken/uses invalid
254 3dc66ebc Iustin Pop
    # protocol; for non-existing agent, get_keys() will just return an
255 3dc66ebc Iustin Pop
    # empty tuple
256 3dc66ebc Iustin Pop
    logging.warning("Can't connect to the ssh agent: %s; skipping its use",
257 3dc66ebc Iustin Pop
                    err)
258 3dc66ebc Iustin Pop
    agent_keys = []
259 3dc66ebc Iustin Pop
260 3dc66ebc Iustin Pop
  return [private_key] + list(agent_keys)
261 3dc66ebc Iustin Pop
262 3dc66ebc Iustin Pop
263 3dc66ebc Iustin Pop
def LoginViaKeys(transport, username, keys):
264 3dc66ebc Iustin Pop
  """Try to login on the given transport via a list of keys.
265 3dc66ebc Iustin Pop
266 3dc66ebc Iustin Pop
  @param transport: the transport to use
267 3dc66ebc Iustin Pop
  @param username: the username to login as
268 3dc66ebc Iustin Pop
  @type keys: list
269 3dc66ebc Iustin Pop
  @param keys: list of C{paramiko.PKey} to use for authentication
270 3dc66ebc Iustin Pop
  @rtype: boolean
271 3dc66ebc Iustin Pop
  @return: True or False depending on whether the login was
272 3dc66ebc Iustin Pop
      successfull or not
273 3dc66ebc Iustin Pop
274 3dc66ebc Iustin Pop
  """
275 3dc66ebc Iustin Pop
  for private_key in keys:
276 3dc66ebc Iustin Pop
    try:
277 3dc66ebc Iustin Pop
      transport.auth_publickey(username, private_key)
278 3dc66ebc Iustin Pop
      fpr = ":".join("%02x" % ord(i) for i in private_key.get_fingerprint())
279 3dc66ebc Iustin Pop
      if isinstance(private_key, paramiko.AgentKey):
280 3dc66ebc Iustin Pop
        logging.debug("Authentication via the ssh-agent key %s", fpr)
281 3dc66ebc Iustin Pop
      else:
282 3dc66ebc Iustin Pop
        logging.debug("Authenticated via public key %s", fpr)
283 3dc66ebc Iustin Pop
      return True
284 3dc66ebc Iustin Pop
    except paramiko.SSHException:
285 3dc66ebc Iustin Pop
      continue
286 3dc66ebc Iustin Pop
  else:
287 3dc66ebc Iustin Pop
    # all keys exhausted
288 3dc66ebc Iustin Pop
    return False
289 3dc66ebc Iustin Pop
290 3dc66ebc Iustin Pop
291 3dc66ebc Iustin Pop
def main():
292 3dc66ebc Iustin Pop
  """Main routine.
293 3dc66ebc Iustin Pop
294 3dc66ebc Iustin Pop
  """
295 3dc66ebc Iustin Pop
  (options, args) = ParseOptions()
296 3dc66ebc Iustin Pop
297 3dc66ebc Iustin Pop
  SetupLogging(options)
298 3dc66ebc Iustin Pop
299 3dc66ebc Iustin Pop
  all_keys = LoadPrivateKeys(options)
300 3dc66ebc Iustin Pop
301 7a6a27af Iustin Pop
  passwd = None
302 7a6a27af Iustin Pop
  username = constants.GANETI_RUNAS
303 7bff16bd Iustin Pop
  ssh_port = netutils.GetDaemonPort("ssh")
304 05cd934d René Nussbaumer
305 8647a52c Iustin Pop
  # Below, we need to join() the transport objects, as otherwise the
306 8647a52c Iustin Pop
  # following happens:
307 8647a52c Iustin Pop
  # - the main thread finishes
308 8647a52c Iustin Pop
  # - the atexit functions run (in the main thread), and cause the
309 8647a52c Iustin Pop
  #   logging file to be closed
310 8647a52c Iustin Pop
  # - a tiny bit later, the transport thread is finally ending, and
311 8647a52c Iustin Pop
  #   wants to log one more message, which fails as the file is closed
312 8647a52c Iustin Pop
  #   now
313 8647a52c Iustin Pop
314 05cd934d René Nussbaumer
  for host in args:
315 7bff16bd Iustin Pop
    transport = paramiko.Transport((host, ssh_port))
316 7a6a27af Iustin Pop
    transport.start_client()
317 8647a52c Iustin Pop
    try:
318 3dc66ebc Iustin Pop
      if LoginViaKeys(transport, username, all_keys):
319 7a6a27af Iustin Pop
        logging.info("Authenticated to %s via public key", host)
320 3dc66ebc Iustin Pop
      else:
321 7a6a27af Iustin Pop
        logging.warning("Authentication to %s via public key failed, trying"
322 7a6a27af Iustin Pop
                        " password", host)
323 7a6a27af Iustin Pop
        if passwd is None:
324 7a6a27af Iustin Pop
          passwd = getpass.getpass(prompt="%s password:" % username)
325 7a6a27af Iustin Pop
        transport.auth_password(username=username, password=passwd)
326 7a6a27af Iustin Pop
        logging.info("Authenticated to %s via password", host)
327 7a6a27af Iustin Pop
    except paramiko.SSHException, err:
328 8647a52c Iustin Pop
      logging.error("Connection or authentication failed to host %s: %s",
329 8647a52c Iustin Pop
                    host, err)
330 8647a52c Iustin Pop
      transport.close()
331 8647a52c Iustin Pop
      # this is needed for compatibility with older Paramiko or Python
332 8647a52c Iustin Pop
      # versions
333 8647a52c Iustin Pop
      transport.join()
334 8647a52c Iustin Pop
      continue
335 05cd934d René Nussbaumer
    try:
336 05cd934d René Nussbaumer
      try:
337 05cd934d René Nussbaumer
        SetupSSH(transport)
338 05cd934d René Nussbaumer
        SetupNodeDaemon(transport)
339 05cd934d René Nussbaumer
      except errors.GenericError, err:
340 8647a52c Iustin Pop
        logging.error("While doing setup on host %s an error occured: %s",
341 8647a52c Iustin Pop
                      host, err)
342 05cd934d René Nussbaumer
    finally:
343 05cd934d René Nussbaumer
      transport.close()
344 8647a52c Iustin Pop
      # this is needed for compatibility with older Paramiko or Python
345 8647a52c Iustin Pop
      # versions
346 8647a52c Iustin Pop
      transport.join()
347 05cd934d René Nussbaumer
348 05cd934d René Nussbaumer
349 05cd934d René Nussbaumer
if __name__ == "__main__":
350 05cd934d René Nussbaumer
  main()