Statistics
| Branch: | Tag: | Revision:

root / lib / ssh.py @ 196ec587

History | View | Annotate | Download (7.9 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 mkdir:
57
    utils.EnsureDirs([(ssh_dir, constants.SECURE_DIR_MODE)])
58
  elif not os.path.isdir(ssh_dir):
59
    raise errors.OpExecError("path ~%s/.ssh is not a directory" % user)
60

    
61
  return [utils.PathJoin(ssh_dir, base)
62
          for base in ["id_dsa", "id_dsa.pub", "authorized_keys"]]
63

    
64

    
65
class SshRunner:
66
  """Wrapper for SSH commands.
67

68
  """
69
  def __init__(self, cluster_name):
70
    self.cluster_name = cluster_name
71

    
72
  def _BuildSshOptions(self, batch, ask_key, use_cluster_key,
73
                       strict_host_check, private_key=None, quiet=True):
74
    """Builds a list with needed SSH options.
75

76
    @param batch: same as ssh's batch option
77
    @param ask_key: allows ssh to ask for key confirmation; this
78
        parameter conflicts with the batch one
79
    @param use_cluster_key: if True, use the cluster name as the
80
        HostKeyAlias name
81
    @param strict_host_check: this makes the host key checking strict
82
    @param private_key: use this private key instead of the default
83
    @param quiet: whether to enable -q to ssh
84

85
    @rtype: list
86
    @return: the list of options ready to use in L{utils.RunCmd}
87

88
    """
89
    options = [
90
      "-oEscapeChar=none",
91
      "-oHashKnownHosts=no",
92
      "-oGlobalKnownHostsFile=%s" % constants.SSH_KNOWN_HOSTS_FILE,
93
      "-oUserKnownHostsFile=/dev/null",
94
      "-oCheckHostIp=no",
95
      ]
96

    
97
    if use_cluster_key:
98
      options.append("-oHostKeyAlias=%s" % self.cluster_name)
99

    
100
    if quiet:
101
      options.append("-q")
102

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

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

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

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

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

    
121
    else:
122
      # non-batch mode
123

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

    
131
    return options
132

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

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

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

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

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

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

169
    Args: see SshRunner.BuildCmd.
170

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

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

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

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

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

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

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

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

    
200
    result = utils.RunCmd(command)
201

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

    
207
    return not result.failed
208

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

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

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

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

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

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

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

    
240
    remotehostname = retval.stdout.strip()
241

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

    
250
    return True, "host matches"
251

    
252

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

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