Statistics
| Branch: | Tag: | Revision:

root / lib / ssh.py @ 33993ab8

History | View | Annotate | Download (8.2 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007 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

    
22
"""Module encapsulating ssh functionality.
23

24
"""
25

    
26

    
27
import os
28
import logging
29
import re
30

    
31
from ganeti import utils
32
from ganeti import errors
33
from ganeti import constants
34

    
35

    
36
def FormatParamikoFingerprint(fingerprint):
37
  """Formats the fingerprint of L{paramiko.PKey.get_fingerprint()}
38

39
  @type fingerprint: str
40
  @param fingerprint: PKey fingerprint
41
  @return The string hex representation of the fingerprint
42

43
  """
44
  assert len(fingerprint) % 2 == 0
45
  return ":".join(re.findall(r"..", fingerprint.lower()))
46

    
47

    
48
def GetUserFiles(user, mkdir=False):
49
  """Return the paths of a user's ssh files.
50

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

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

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

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

    
68
  ssh_dir = utils.PathJoin(user_dir, ".ssh")
69
  if mkdir:
70
    utils.EnsureDirs([(ssh_dir, constants.SECURE_DIR_MODE)])
71
  elif not os.path.isdir(ssh_dir):
72
    raise errors.OpExecError("Path %s is not a directory" % ssh_dir)
73

    
74
  return [utils.PathJoin(ssh_dir, base)
75
          for base in ["id_dsa", "id_dsa.pub", "authorized_keys"]]
76

    
77

    
78
class SshRunner:
79
  """Wrapper for SSH commands.
80

81
  """
82
  def __init__(self, cluster_name):
83
    self.cluster_name = cluster_name
84

    
85
  def _BuildSshOptions(self, batch, ask_key, use_cluster_key,
86
                       strict_host_check, private_key=None, quiet=True):
87
    """Builds a list with needed SSH options.
88

89
    @param batch: same as ssh's batch option
90
    @param ask_key: allows ssh to ask for key confirmation; this
91
        parameter conflicts with the batch one
92
    @param use_cluster_key: if True, use the cluster name as the
93
        HostKeyAlias name
94
    @param strict_host_check: this makes the host key checking strict
95
    @param private_key: use this private key instead of the default
96
    @param quiet: whether to enable -q to ssh
97

98
    @rtype: list
99
    @return: the list of options ready to use in L{utils.RunCmd}
100

101
    """
102
    options = [
103
      "-oEscapeChar=none",
104
      "-oHashKnownHosts=no",
105
      "-oGlobalKnownHostsFile=%s" % constants.SSH_KNOWN_HOSTS_FILE,
106
      "-oUserKnownHostsFile=/dev/null",
107
      "-oCheckHostIp=no",
108
      ]
109

    
110
    if use_cluster_key:
111
      options.append("-oHostKeyAlias=%s" % self.cluster_name)
112

    
113
    if quiet:
114
      options.append("-q")
115

    
116
    if private_key:
117
      options.append("-i%s" % private_key)
118

    
119
    # TODO: Too many boolean options, maybe convert them to more descriptive
120
    # constants.
121

    
122
    # Note: ask_key conflicts with batch mode
123
    if batch:
124
      if ask_key:
125
        raise errors.ProgrammerError("SSH call requested conflicting options")
126

    
127
      options.append("-oBatchMode=yes")
128

    
129
      if strict_host_check:
130
        options.append("-oStrictHostKeyChecking=yes")
131
      else:
132
        options.append("-oStrictHostKeyChecking=no")
133

    
134
    else:
135
      # non-batch mode
136

    
137
      if ask_key:
138
        options.append("-oStrictHostKeyChecking=ask")
139
      elif strict_host_check:
140
        options.append("-oStrictHostKeyChecking=yes")
141
      else:
142
        options.append("-oStrictHostKeyChecking=no")
143

    
144
    return options
145

    
146
  def BuildCmd(self, hostname, user, command, batch=True, ask_key=False,
147
               tty=False, use_cluster_key=True, strict_host_check=True,
148
               private_key=None, quiet=True):
149
    """Build an ssh command to execute a command on a remote node.
150

151
    @param hostname: the target host, string
152
    @param user: user to auth as
153
    @param command: the command
154
    @param batch: if true, ssh will run in batch mode with no prompting
155
    @param ask_key: if true, ssh will run with
156
        StrictHostKeyChecking=ask, so that we can connect to an
157
        unknown host (not valid in batch mode)
158
    @param use_cluster_key: whether to expect and use the
159
        cluster-global SSH key
160
    @param strict_host_check: whether to check the host's SSH key at all
161
    @param private_key: use this private key instead of the default
162
    @param quiet: whether to enable -q to ssh
163

164
    @return: the ssh call to run 'command' on the remote host.
165

166
    """
167
    argv = [constants.SSH]
168
    argv.extend(self._BuildSshOptions(batch, ask_key, use_cluster_key,
169
                                      strict_host_check, private_key,
170
                                      quiet=quiet))
171
    if tty:
172
      argv.extend(["-t", "-t"])
173
    argv.extend(["%s@%s" % (user, hostname), command])
174
    return argv
175

    
176
  def Run(self, *args, **kwargs):
177
    """Runs a command on a remote node.
178

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

182
    Args: see SshRunner.BuildCmd.
183

184
    @rtype: L{utils.RunResult}
185
    @return: the result as from L{utils.RunCmd()}
186

187
    """
188
    return utils.RunCmd(self.BuildCmd(*args, **kwargs))
189

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

193
    @param node: node in the cluster
194
    @param filename: absolute pathname of a local file
195

196
    @rtype: boolean
197
    @return: the success of the operation
198

199
    """
200
    if not os.path.isabs(filename):
201
      logging.error("File %s must be an absolute path", filename)
202
      return False
203

    
204
    if not os.path.isfile(filename):
205
      logging.error("File %s does not exist", filename)
206
      return False
207

    
208
    command = [constants.SCP, "-p"]
209
    command.extend(self._BuildSshOptions(True, False, True, True))
210
    command.append(filename)
211
    command.append("%s:%s" % (node, filename))
212

    
213
    result = utils.RunCmd(command)
214

    
215
    if result.failed:
216
      logging.error("Copy to node %s failed (%s) error %s,"
217
                    " command was %s",
218
                    node, result.fail_reason, result.output, result.cmd)
219

    
220
    return not result.failed
221

    
222
  def VerifyNodeHostname(self, node):
223
    """Verify hostname consistency via SSH.
224

225
    This functions connects via ssh to a node and compares the hostname
226
    reported by the node to the name with have (the one that we
227
    connected to).
228

229
    This is used to detect problems in ssh known_hosts files
230
    (conflicting known hosts) and inconsistencies between dns/hosts
231
    entries and local machine names
232

233
    @param node: nodename of a host to check; can be short or
234
        full qualified hostname
235

236
    @return: (success, detail), where:
237
        - success: True/False
238
        - detail: string with details
239

240
    """
241
    retval = self.Run(node, 'root', 'hostname --fqdn')
242

    
243
    if retval.failed:
244
      msg = "ssh problem"
245
      output = retval.output
246
      if output:
247
        msg += ": %s" % output
248
      else:
249
        msg += ": %s (no output)" % retval.fail_reason
250
      logging.error("Command %s failed: %s", retval.cmd, msg)
251
      return False, msg
252

    
253
    remotehostname = retval.stdout.strip()
254

    
255
    if not remotehostname or remotehostname != node:
256
      if node.startswith(remotehostname + "."):
257
        msg = "hostname not FQDN"
258
      else:
259
        msg = "hostname mismatch"
260
      return False, ("%s: expected %s but got %s" %
261
                     (msg, node, remotehostname))
262

    
263
    return True, "host matches"
264

    
265

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

269
  """
270
  utils.WriteFile(file_name, mode=0600,
271
                  data="%s ssh-rsa %s\n" % (cfg.GetClusterName(),
272
                                            cfg.GetHostKey()))