Statistics
| Branch: | Tag: | Revision:

root / tools / setup-ssh @ 634a9a35

History | View | Annotate | Download (10.5 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 634a9a35 Iustin Pop
    # Due to the way SFTPFile and BufferedFile are implemented,
141 634a9a35 Iustin Pop
    # opening in a+ mode and then issuing a read(), readline() or
142 634a9a35 Iustin Pop
    # iterating over the file (which uses read() internally) will see
143 634a9a35 Iustin Pop
    # an empty file, since the paramiko internal file position and the
144 634a9a35 Iustin Pop
    # OS-level file-position are desynchronized; therefore, we issue
145 634a9a35 Iustin Pop
    # an explicit seek to resynchronize these; writes should (note
146 634a9a35 Iustin Pop
    # should) still go to the right place
147 634a9a35 Iustin Pop
    authorized_keys.seek(0, 0)
148 05cd934d René Nussbaumer
    # We don't have to close, as the close happened already in AddAuthorizedKey
149 05cd934d René Nussbaumer
    utils.AddAuthorizedKey(authorized_keys, filemap[pub_key][0])
150 05cd934d René Nussbaumer
  finally:
151 05cd934d René Nussbaumer
    authorized_keys.close()
152 05cd934d René Nussbaumer
153 05cd934d René Nussbaumer
  _InvokeDaemonUtil(transport, "reload-ssh-keys")
154 05cd934d René Nussbaumer
155 05cd934d René Nussbaumer
156 05cd934d René Nussbaumer
def SetupNodeDaemon(transport):
157 05cd934d René Nussbaumer
  """Sets the node daemon up on the other side.
158 05cd934d René Nussbaumer
159 05cd934d René Nussbaumer
  @param transport: The paramiko transport instance
160 05cd934d René Nussbaumer
161 05cd934d René Nussbaumer
  """
162 05cd934d René Nussbaumer
  noded_cert = utils.ReadFile(constants.NODED_CERT_FILE)
163 05cd934d René Nussbaumer
164 05cd934d René Nussbaumer
  sftp = transport.open_sftp_client()
165 05cd934d René Nussbaumer
  _WriteSftpFile(sftp, constants.NODED_CERT_FILE, 0400, noded_cert)
166 05cd934d René Nussbaumer
167 05cd934d René Nussbaumer
  _InvokeDaemonUtil(transport, "start %s" % constants.NODED)
168 05cd934d René Nussbaumer
169 05cd934d René Nussbaumer
170 05cd934d René Nussbaumer
def ParseOptions():
171 05cd934d René Nussbaumer
  """Parses options passed to program.
172 05cd934d René Nussbaumer
173 05cd934d René Nussbaumer
  """
174 05cd934d René Nussbaumer
  program = os.path.basename(sys.argv[0])
175 05cd934d René Nussbaumer
176 05cd934d René Nussbaumer
  parser = optparse.OptionParser(usage=("%prog [--debug|--verbose] <node>"
177 05cd934d René Nussbaumer
                                        " <node...>"), prog=program)
178 05cd934d René Nussbaumer
  parser.add_option(cli.DEBUG_OPT)
179 05cd934d René Nussbaumer
  parser.add_option(cli.VERBOSE_OPT)
180 7a6a27af Iustin Pop
  default_key = ssh.GetUserFiles(constants.GANETI_RUNAS)[0]
181 7a6a27af Iustin Pop
  parser.add_option(optparse.Option("-f", dest="private_key",
182 7a6a27af Iustin Pop
                                    default=default_key,
183 7a6a27af Iustin Pop
                                    help="The private key to (try to) use for"
184 7a6a27af Iustin Pop
                                    "authentication "))
185 7a6a27af Iustin Pop
  parser.add_option(optparse.Option("--key-type", dest="key_type",
186 7a6a27af Iustin Pop
                                    choices=("rsa", "dsa"), default="dsa",
187 7a6a27af Iustin Pop
                                    help="The private key type (rsa or dsa)"))
188 05cd934d René Nussbaumer
189 05cd934d René Nussbaumer
  (options, args) = parser.parse_args()
190 05cd934d René Nussbaumer
191 05cd934d René Nussbaumer
  return (options, args)
192 05cd934d René Nussbaumer
193 05cd934d René Nussbaumer
194 05cd934d René Nussbaumer
def SetupLogging(options):
195 05cd934d René Nussbaumer
  """Sets up the logging.
196 05cd934d René Nussbaumer
197 05cd934d René Nussbaumer
  @param options: Parsed options
198 05cd934d René Nussbaumer
199 05cd934d René Nussbaumer
  """
200 05cd934d René Nussbaumer
  fmt = "%(asctime)s: %(threadName)s "
201 05cd934d René Nussbaumer
  if options.debug or options.verbose:
202 05cd934d René Nussbaumer
    fmt += "%(levelname)s "
203 05cd934d René Nussbaumer
  fmt += "%(message)s"
204 05cd934d René Nussbaumer
205 05cd934d René Nussbaumer
  formatter = logging.Formatter(fmt)
206 05cd934d René Nussbaumer
207 05cd934d René Nussbaumer
  file_handler = logging.FileHandler(constants.LOG_SETUP_SSH)
208 05cd934d René Nussbaumer
  stderr_handler = logging.StreamHandler()
209 05cd934d René Nussbaumer
  stderr_handler.setFormatter(formatter)
210 05cd934d René Nussbaumer
  file_handler.setFormatter(formatter)
211 5c654e95 Iustin Pop
  file_handler.setLevel(logging.INFO)
212 05cd934d René Nussbaumer
213 05cd934d René Nussbaumer
  if options.debug:
214 5c654e95 Iustin Pop
    stderr_handler.setLevel(logging.DEBUG)
215 05cd934d René Nussbaumer
  elif options.verbose:
216 05cd934d René Nussbaumer
    stderr_handler.setLevel(logging.INFO)
217 05cd934d René Nussbaumer
  else:
218 5c654e95 Iustin Pop
    stderr_handler.setLevel(logging.WARNING)
219 05cd934d René Nussbaumer
220 05cd934d René Nussbaumer
  root_logger = logging.getLogger("")
221 3dc66ebc Iustin Pop
  root_logger.setLevel(logging.NOTSET)
222 05cd934d René Nussbaumer
  root_logger.addHandler(stderr_handler)
223 05cd934d René Nussbaumer
  root_logger.addHandler(file_handler)
224 8647a52c Iustin Pop
225 8647a52c Iustin Pop
  # This is the paramiko logger instance
226 8647a52c Iustin Pop
  paramiko_logger = logging.getLogger("paramiko")
227 05cd934d René Nussbaumer
  paramiko_logger.addHandler(file_handler)
228 5c654e95 Iustin Pop
  # We don't want to debug Paramiko, so filter anything below warning
229 5c654e95 Iustin Pop
  paramiko_logger.setLevel(logging.WARNING)
230 05cd934d René Nussbaumer
231 05cd934d René Nussbaumer
232 3dc66ebc Iustin Pop
def LoadPrivateKeys(options):
233 3dc66ebc Iustin Pop
  """Load the list of available private keys
234 05cd934d René Nussbaumer
235 3dc66ebc Iustin Pop
  It loads the standard ssh key from disk and then tries to connect to
236 3dc66ebc Iustin Pop
  the ssh agent too.
237 05cd934d René Nussbaumer
238 3dc66ebc Iustin Pop
  @rtype: list
239 3dc66ebc Iustin Pop
  @return: a list of C{paramiko.PKey}
240 05cd934d René Nussbaumer
241 3dc66ebc Iustin Pop
  """
242 7a6a27af Iustin Pop
  if options.key_type == "rsa":
243 7a6a27af Iustin Pop
    pkclass = paramiko.RSAKey
244 7a6a27af Iustin Pop
  elif options.key_type == "dsa":
245 7a6a27af Iustin Pop
    pkclass = paramiko.DSSKey
246 7a6a27af Iustin Pop
  else:
247 7a6a27af Iustin Pop
    logging.critical("Unknown key type %s selected (choose either rsa or dsa)",
248 7a6a27af Iustin Pop
                     options.key_type)
249 7a6a27af Iustin Pop
    sys.exit(1)
250 7a6a27af Iustin Pop
251 7a6a27af Iustin Pop
  try:
252 7a6a27af Iustin Pop
    private_key = pkclass.from_private_key_file(options.private_key)
253 7a6a27af Iustin Pop
  except (paramiko.SSHException, EnvironmentError), err:
254 7a6a27af Iustin Pop
    logging.critical("Can't load private key %s: %s", options.private_key, err)
255 7a6a27af Iustin Pop
    sys.exit(1)
256 7a6a27af Iustin Pop
257 3dc66ebc Iustin Pop
  try:
258 3dc66ebc Iustin Pop
    agent = paramiko.Agent()
259 3dc66ebc Iustin Pop
    agent_keys = agent.get_keys()
260 3dc66ebc Iustin Pop
  except paramiko.SSHException, err:
261 3dc66ebc Iustin Pop
    # this will only be seen when the agent is broken/uses invalid
262 3dc66ebc Iustin Pop
    # protocol; for non-existing agent, get_keys() will just return an
263 3dc66ebc Iustin Pop
    # empty tuple
264 3dc66ebc Iustin Pop
    logging.warning("Can't connect to the ssh agent: %s; skipping its use",
265 3dc66ebc Iustin Pop
                    err)
266 3dc66ebc Iustin Pop
    agent_keys = []
267 3dc66ebc Iustin Pop
268 3dc66ebc Iustin Pop
  return [private_key] + list(agent_keys)
269 3dc66ebc Iustin Pop
270 3dc66ebc Iustin Pop
271 3dc66ebc Iustin Pop
def LoginViaKeys(transport, username, keys):
272 3dc66ebc Iustin Pop
  """Try to login on the given transport via a list of keys.
273 3dc66ebc Iustin Pop
274 3dc66ebc Iustin Pop
  @param transport: the transport to use
275 3dc66ebc Iustin Pop
  @param username: the username to login as
276 3dc66ebc Iustin Pop
  @type keys: list
277 3dc66ebc Iustin Pop
  @param keys: list of C{paramiko.PKey} to use for authentication
278 3dc66ebc Iustin Pop
  @rtype: boolean
279 3dc66ebc Iustin Pop
  @return: True or False depending on whether the login was
280 3dc66ebc Iustin Pop
      successfull or not
281 3dc66ebc Iustin Pop
282 3dc66ebc Iustin Pop
  """
283 3dc66ebc Iustin Pop
  for private_key in keys:
284 3dc66ebc Iustin Pop
    try:
285 3dc66ebc Iustin Pop
      transport.auth_publickey(username, private_key)
286 3dc66ebc Iustin Pop
      fpr = ":".join("%02x" % ord(i) for i in private_key.get_fingerprint())
287 3dc66ebc Iustin Pop
      if isinstance(private_key, paramiko.AgentKey):
288 3dc66ebc Iustin Pop
        logging.debug("Authentication via the ssh-agent key %s", fpr)
289 3dc66ebc Iustin Pop
      else:
290 3dc66ebc Iustin Pop
        logging.debug("Authenticated via public key %s", fpr)
291 3dc66ebc Iustin Pop
      return True
292 3dc66ebc Iustin Pop
    except paramiko.SSHException:
293 3dc66ebc Iustin Pop
      continue
294 3dc66ebc Iustin Pop
  else:
295 3dc66ebc Iustin Pop
    # all keys exhausted
296 3dc66ebc Iustin Pop
    return False
297 3dc66ebc Iustin Pop
298 3dc66ebc Iustin Pop
299 3dc66ebc Iustin Pop
def main():
300 3dc66ebc Iustin Pop
  """Main routine.
301 3dc66ebc Iustin Pop
302 3dc66ebc Iustin Pop
  """
303 3dc66ebc Iustin Pop
  (options, args) = ParseOptions()
304 3dc66ebc Iustin Pop
305 3dc66ebc Iustin Pop
  SetupLogging(options)
306 3dc66ebc Iustin Pop
307 3dc66ebc Iustin Pop
  all_keys = LoadPrivateKeys(options)
308 3dc66ebc Iustin Pop
309 7a6a27af Iustin Pop
  passwd = None
310 7a6a27af Iustin Pop
  username = constants.GANETI_RUNAS
311 7bff16bd Iustin Pop
  ssh_port = netutils.GetDaemonPort("ssh")
312 05cd934d René Nussbaumer
313 8647a52c Iustin Pop
  # Below, we need to join() the transport objects, as otherwise the
314 8647a52c Iustin Pop
  # following happens:
315 8647a52c Iustin Pop
  # - the main thread finishes
316 8647a52c Iustin Pop
  # - the atexit functions run (in the main thread), and cause the
317 8647a52c Iustin Pop
  #   logging file to be closed
318 8647a52c Iustin Pop
  # - a tiny bit later, the transport thread is finally ending, and
319 8647a52c Iustin Pop
  #   wants to log one more message, which fails as the file is closed
320 8647a52c Iustin Pop
  #   now
321 8647a52c Iustin Pop
322 05cd934d René Nussbaumer
  for host in args:
323 7bff16bd Iustin Pop
    transport = paramiko.Transport((host, ssh_port))
324 7a6a27af Iustin Pop
    transport.start_client()
325 8647a52c Iustin Pop
    try:
326 3dc66ebc Iustin Pop
      if LoginViaKeys(transport, username, all_keys):
327 7a6a27af Iustin Pop
        logging.info("Authenticated to %s via public key", host)
328 3dc66ebc Iustin Pop
      else:
329 7a6a27af Iustin Pop
        logging.warning("Authentication to %s via public key failed, trying"
330 7a6a27af Iustin Pop
                        " password", host)
331 7a6a27af Iustin Pop
        if passwd is None:
332 7a6a27af Iustin Pop
          passwd = getpass.getpass(prompt="%s password:" % username)
333 7a6a27af Iustin Pop
        transport.auth_password(username=username, password=passwd)
334 7a6a27af Iustin Pop
        logging.info("Authenticated to %s via password", host)
335 7a6a27af Iustin Pop
    except paramiko.SSHException, err:
336 8647a52c Iustin Pop
      logging.error("Connection or authentication failed to host %s: %s",
337 8647a52c Iustin Pop
                    host, err)
338 8647a52c Iustin Pop
      transport.close()
339 8647a52c Iustin Pop
      # this is needed for compatibility with older Paramiko or Python
340 8647a52c Iustin Pop
      # versions
341 8647a52c Iustin Pop
      transport.join()
342 8647a52c Iustin Pop
      continue
343 05cd934d René Nussbaumer
    try:
344 05cd934d René Nussbaumer
      try:
345 05cd934d René Nussbaumer
        SetupSSH(transport)
346 05cd934d René Nussbaumer
        SetupNodeDaemon(transport)
347 05cd934d René Nussbaumer
      except errors.GenericError, err:
348 8647a52c Iustin Pop
        logging.error("While doing setup on host %s an error occured: %s",
349 8647a52c Iustin Pop
                      host, err)
350 05cd934d René Nussbaumer
    finally:
351 05cd934d René Nussbaumer
      transport.close()
352 8647a52c Iustin Pop
      # this is needed for compatibility with older Paramiko or Python
353 8647a52c Iustin Pop
      # versions
354 8647a52c Iustin Pop
      transport.join()
355 05cd934d René Nussbaumer
356 05cd934d René Nussbaumer
357 05cd934d René Nussbaumer
if __name__ == "__main__":
358 05cd934d René Nussbaumer
  main()