ssh.py 6.93 KB
Newer Older
Iustin Pop's avatar
Iustin Pop committed
1
#
Iustin Pop's avatar
Iustin Pop committed
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#

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


"""Module encapsulating ssh functionality.

"""


import os

from ganeti import logger
from ganeti import utils
from ganeti import errors
Iustin Pop's avatar
Iustin Pop committed
32
from ganeti import constants
33
from ganeti import ssconf
Iustin Pop's avatar
Iustin Pop committed
34
35
36
37
38
39
40
41
42
43


KNOWN_HOSTS_OPTS = [
  "-oGlobalKnownHostsFile=%s" % constants.SSH_KNOWN_HOSTS_FILE,
  "-oUserKnownHostsFile=/dev/null",
  ]

# Note: BATCH_MODE conflicts with ASK_KEY
BATCH_MODE_OPTS = [
  "-oBatchMode=yes",
44
  "-oEscapeChar=none",
Iustin Pop's avatar
Iustin Pop committed
45
46
47
48
49
50
  "-oStrictHostKeyChecking=yes",
  ]

ASK_KEY_OPTS = [
  "-oEscapeChar=none",
  "-oHashKnownHosts=no",
51
  "-oStrictHostKeyChecking=ask",
Iustin Pop's avatar
Iustin Pop committed
52
53
  ]

54

55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def GetUserFiles(user, mkdir=False):
  """Return the paths of a user's ssh files.

  The function will return a triplet (priv_key_path, pub_key_path,
  auth_key_path) that are used for ssh authentication. Currently, the
  keys used are DSA keys, so this function will return:
  (~user/.ssh/id_dsa, ~user/.ssh/id_dsa.pub,
  ~user/.ssh/authorized_keys).

  If the optional parameter mkdir is True, the ssh directory will be
  created if it doesn't exist.

  Regardless of the mkdir parameters, the script will raise an error
  if ~user/.ssh is not a directory.

  """
  user_dir = utils.GetHomeDir(user)
  if not user_dir:
    raise errors.OpExecError("Cannot resolve home of user %s" % user)

  ssh_dir = os.path.join(user_dir, ".ssh")
  if not os.path.lexists(ssh_dir):
    if mkdir:
      try:
        os.mkdir(ssh_dir, 0700)
      except EnvironmentError, err:
        raise errors.OpExecError("Can't create .ssh dir for user %s: %s" %
                                 (user, str(err)))
  elif not os.path.isdir(ssh_dir):
    raise errors.OpExecError("path ~%s/.ssh is not a directory" % user)

  return [os.path.join(ssh_dir, base)
          for base in ["id_dsa", "id_dsa.pub", "authorized_keys"]]


90
91
class SshRunner:
  """Wrapper for SSH commands.
Iustin Pop's avatar
Iustin Pop committed
92
93

  """
94
95
96
97
98
99
100
101
102
  def __init__(self, sstore=None):
    if sstore is None:
      self.sstore = ssconf.SimpleStore()
    else:
      self.sstore = sstore

  def _GetHostKeyAliasOption(self):
    return "-oHostKeyAlias=%s" % self.sstore.GetClusterName()

103
  def BuildCmd(self, hostname, user, command, batch=True, ask_key=False,
Michael Hanselmann's avatar
Michael Hanselmann committed
104
               tty=False, use_cluster_key=True):
105
106
107
108
109
110
111
112
113
    """Build an ssh command to execute a command on a remote node.

    Args:
      hostname: the target host, string
      user: user to auth as
      command: the command
      batch: if true, ssh will run in batch mode with no prompting
      ask_key: if true, ssh will run with StrictHostKeyChecking=ask, so that
               we can connect to an unknown host (not valid in batch mode)
Michael Hanselmann's avatar
Michael Hanselmann committed
114
      use_cluster_key: Whether to expect and use the cluster-global SSH key
115
116
117
118
119

    Returns:
      The ssh call to run 'command' on the remote host.

    """
120
    argv = [constants.SSH, "-q"]
121
    argv.extend(KNOWN_HOSTS_OPTS)
Michael Hanselmann's avatar
Michael Hanselmann committed
122
123
    if use_cluster_key:
      argv.append(self._GetHostKeyAliasOption())
124
125
126
127
128
129
130
    if batch:
      # if we are in batch mode, we can't ask the key
      if ask_key:
        raise errors.ProgrammerError("SSH call requested conflicting options")
      argv.extend(BATCH_MODE_OPTS)
    elif ask_key:
      argv.extend(ASK_KEY_OPTS)
131
132
    if tty:
      argv.append("-t")
133
134
135
    argv.extend(["%s@%s" % (user, hostname), command])
    return argv

Michael Hanselmann's avatar
Michael Hanselmann committed
136
137
  def Run(self, hostname, user, command, batch=True, ask_key=False,
          use_cluster_key=True):
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
    """Runs a command on a remote node.

    This method has the same return value as `utils.RunCmd()`, which it
    uses to launch ssh.

    Args:
      hostname: the target host, string
      user: user to auth as
      command: the command
      batch: if true, ssh will run in batch mode with no prompting
      ask_key: if true, ssh will run with StrictHostKeyChecking=ask, so that
               we can connect to an unknown host (not valid in batch mode)

    Returns:
      `utils.RunResult` like `utils.RunCmd()`

    """
    return utils.RunCmd(self.BuildCmd(hostname, user, command, batch=batch,
Michael Hanselmann's avatar
Michael Hanselmann committed
156
157
                                      ask_key=ask_key,
                                      use_cluster_key=use_cluster_key))
158
159
160
161
162
163
164
165
166
167

  def CopyFileToNode(self, node, filename):
    """Copy a file to another node with scp.

    Args:
      node: node in the cluster
      filename: absolute pathname of a local file

    Returns:
      success: True/False
Iustin Pop's avatar
Iustin Pop committed
168

169
170
171
172
    """
    if not os.path.isabs(filename):
      logger.Error("file %s must be an absolute path" % (filename))
      return False
Iustin Pop's avatar
Iustin Pop committed
173

174
175
176
177
    if not os.path.isfile(filename):
      logger.Error("file %s does not exist" % (filename))
      return False

178
    command = [constants.SCP, "-q", "-p"]
179
180
    command.extend(KNOWN_HOSTS_OPTS)
    command.extend(BATCH_MODE_OPTS)
181
    command.append(self._GetHostKeyAliasOption())
182
183
    command.append(filename)
    command.append("%s:%s" % (node, filename))
Iustin Pop's avatar
Iustin Pop committed
184

185
    result = utils.RunCmd(command)
Iustin Pop's avatar
Iustin Pop committed
186

187
188
189
190
    if result.failed:
      logger.Error("copy to node %s failed (%s) error %s,"
                   " command was %s" %
                   (node, result.fail_reason, result.output, result.cmd))
Iustin Pop's avatar
Iustin Pop committed
191

192
    return not result.failed
Iustin Pop's avatar
Iustin Pop committed
193

194
195
  def VerifyNodeHostname(self, node):
    """Verify hostname consistency via SSH.
Iustin Pop's avatar
Iustin Pop committed
196

197
198
199
    This functions connects via ssh to a node and compares the hostname
    reported by the node to the name with have (the one that we
    connected to).
Iustin Pop's avatar
Iustin Pop committed
200

201
202
203
    This is used to detect problems in ssh known_hosts files
    (conflicting known hosts) and incosistencies between dns/hosts
    entries and local machine names
Iustin Pop's avatar
Iustin Pop committed
204

205
206
    Args:
      node: nodename of a host to check. can be short or full qualified hostname
Iustin Pop's avatar
Iustin Pop committed
207

208
209
210
211
212
    Returns:
      (success, detail)
      where
        success: True/False
        detail: String with details
Iustin Pop's avatar
Iustin Pop committed
213

214
215
    """
    retval = self.Run(node, 'root', 'hostname')
Iustin Pop's avatar
Iustin Pop committed
216

217
218
219
220
221
222
    if retval.failed:
      msg = "ssh problem"
      output = retval.output
      if output:
        msg += ": %s" % output
      return False, msg
Iustin Pop's avatar
Iustin Pop committed
223

224
    remotehostname = retval.stdout.strip()
Iustin Pop's avatar
Iustin Pop committed
225

226
227
    if not remotehostname or remotehostname != node:
      return False, "hostname mismatch, got %s" % remotehostname
Iustin Pop's avatar
Iustin Pop committed
228

229
    return True, "host matches"
230
231
232
233
234
235
236
237
238


def WriteKnownHostsFile(cfg, sstore, file_name):
  """Writes the cluster-wide equally known_hosts file.

  """
  utils.WriteFile(file_name, mode=0700,
                  data="%s ssh-rsa %s\n" % (sstore.GetClusterName(),
                                            cfg.GetHostKey()))