Statistics
| Branch: | Tag: | Revision:

root / lib / ssh.py @ b427788e

History | View | Annotate | Download (7.8 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

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

    
34

    
35
def GetUserFiles(user, mkdir=False):
36
  """Return the paths of a user's ssh files.
37

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

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

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

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

    
55
  ssh_dir = utils.PathJoin(user_dir, ".ssh")
56
  if not os.path.lexists(ssh_dir):
57
    if mkdir:
58
      try:
59
        os.mkdir(ssh_dir, 0700)
60
      except EnvironmentError, err:
61
        raise errors.OpExecError("Can't create .ssh dir for user %s: %s" %
62
                                 (user, str(err)))
63
  elif not os.path.isdir(ssh_dir):
64
    raise errors.OpExecError("path ~%s/.ssh is not a directory" % user)
65

    
66
  return [utils.PathJoin(ssh_dir, base)
67
          for base in ["id_dsa", "id_dsa.pub", "authorized_keys"]]
68

    
69

    
70
class SshRunner:
71
  """Wrapper for SSH commands.
72

73
  """
74
  def __init__(self, cluster_name):
75
    self.cluster_name = cluster_name
76

    
77
  def _BuildSshOptions(self, batch, ask_key, use_cluster_key,
78
                       strict_host_check, private_key=None):
79
    """Builds a list with needed SSH options.
80

81
    @param batch: same as ssh's batch option
82
    @param ask_key: allows ssh to ask for key confirmation; this
83
        parameter conflicts with the batch one
84
    @param use_cluster_key: if True, use the cluster name as the
85
        HostKeyAlias name
86
    @param strict_host_check: this makes the host key checking strict
87
    @param private_key: use this private key instead of the default
88

89
    @rtype: list
90
    @return: the list of options ready to use in L{utils.RunCmd}
91

92
    """
93
    options = [
94
      "-oEscapeChar=none",
95
      "-oHashKnownHosts=no",
96
      "-oGlobalKnownHostsFile=%s" % constants.SSH_KNOWN_HOSTS_FILE,
97
      "-oUserKnownHostsFile=/dev/null",
98
      "-oCheckHostIp=no",
99
      ]
100

    
101
    if use_cluster_key:
102
      options.append("-oHostKeyAlias=%s" % self.cluster_name)
103

    
104
    if private_key:
105
      options.append("-i%s" % private_key)
106

    
107
    # TODO: Too many boolean options, maybe convert them to more descriptive
108
    # constants.
109

    
110
    # Note: ask_key conflicts with batch mode
111
    if batch:
112
      if ask_key:
113
        raise errors.ProgrammerError("SSH call requested conflicting options")
114

    
115
      options.append("-oBatchMode=yes")
116

    
117
      if strict_host_check:
118
        options.append("-oStrictHostKeyChecking=yes")
119
      else:
120
        options.append("-oStrictHostKeyChecking=no")
121

    
122
    else:
123
      # non-batch mode
124

    
125
      if ask_key:
126
        options.append("-oStrictHostKeyChecking=ask")
127
      elif strict_host_check:
128
        options.append("-oStrictHostKeyChecking=yes")
129
      else:
130
        options.append("-oStrictHostKeyChecking=no")
131

    
132
    return options
133

    
134
  def BuildCmd(self, hostname, user, command, batch=True, ask_key=False,
135
               tty=False, use_cluster_key=True, strict_host_check=True,
136
               private_key=None):
137
    """Build an ssh command to execute a command on a remote node.
138

139
    @param hostname: the target host, string
140
    @param user: user to auth as
141
    @param command: the command
142
    @param batch: if true, ssh will run in batch mode with no prompting
143
    @param ask_key: if true, ssh will run with
144
        StrictHostKeyChecking=ask, so that we can connect to an
145
        unknown host (not valid in batch mode)
146
    @param use_cluster_key: whether to expect and use the
147
        cluster-global SSH key
148
    @param strict_host_check: whether to check the host's SSH key at all
149
    @param private_key: use this private key instead of the default
150

151
    @return: the ssh call to run 'command' on the remote host.
152

153
    """
154
    argv = [constants.SSH, "-q"]
155
    argv.extend(self._BuildSshOptions(batch, ask_key, use_cluster_key,
156
                                      strict_host_check, private_key))
157
    if tty:
158
      argv.append("-t")
159
    argv.extend(["%s@%s" % (user, hostname), command])
160
    return argv
161

    
162
  def Run(self, *args, **kwargs):
163
    """Runs a command on a remote node.
164

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

168
    Args: see SshRunner.BuildCmd.
169

170
    @rtype: L{utils.RunResult}
171
    @return: the result as from L{utils.RunCmd()}
172

173
    """
174
    return utils.RunCmd(self.BuildCmd(*args, **kwargs))
175

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

179
    @param node: node in the cluster
180
    @param filename: absolute pathname of a local file
181

182
    @rtype: boolean
183
    @return: the success of the operation
184

185
    """
186
    if not os.path.isabs(filename):
187
      logging.error("File %s must be an absolute path", filename)
188
      return False
189

    
190
    if not os.path.isfile(filename):
191
      logging.error("File %s does not exist", filename)
192
      return False
193

    
194
    command = [constants.SCP, "-q", "-p"]
195
    command.extend(self._BuildSshOptions(True, False, True, True))
196
    command.append(filename)
197
    command.append("%s:%s" % (node, filename))
198

    
199
    result = utils.RunCmd(command)
200

    
201
    if result.failed:
202
      logging.error("Copy to node %s failed (%s) error %s,"
203
                    " command was %s",
204
                    node, result.fail_reason, result.output, result.cmd)
205

    
206
    return not result.failed
207

    
208
  def VerifyNodeHostname(self, node):
209
    """Verify hostname consistency via SSH.
210

211
    This functions connects via ssh to a node and compares the hostname
212
    reported by the node to the name with have (the one that we
213
    connected to).
214

215
    This is used to detect problems in ssh known_hosts files
216
    (conflicting known hosts) and inconsistencies between dns/hosts
217
    entries and local machine names
218

219
    @param node: nodename of a host to check; can be short or
220
        full qualified hostname
221

222
    @return: (success, detail), where:
223
        - success: True/False
224
        - detail: string with details
225

226
    """
227
    retval = self.Run(node, 'root', 'hostname --fqdn')
228

    
229
    if retval.failed:
230
      msg = "ssh problem"
231
      output = retval.output
232
      if output:
233
        msg += ": %s" % output
234
      else:
235
        msg += ": %s (no output)" % retval.fail_reason
236
      logging.error("Command %s failed: %s", retval.cmd, msg)
237
      return False, msg
238

    
239
    remotehostname = retval.stdout.strip()
240

    
241
    if not remotehostname or remotehostname != node:
242
      if node.startswith(remotehostname + "."):
243
        msg = "hostname not FQDN"
244
      else:
245
        msg = "hostname mistmatch"
246
      return False, ("%s: expected %s but got %s" %
247
                     (msg, node, remotehostname))
248

    
249
    return True, "host matches"
250

    
251

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

255
  """
256
  utils.WriteFile(file_name, mode=0600,
257
                  data="%s ssh-rsa %s\n" % (cfg.GetClusterName(),
258
                                            cfg.GetHostKey()))