Statistics
| Branch: | Tag: | Revision:

root / lib / ssh.py @ 26d3fd2f

History | View | Annotate | Download (8.6 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010 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
from ganeti import netutils
35

    
36

    
37
def FormatParamikoFingerprint(fingerprint):
38
  """Format paramiko PKey fingerprint.
39

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

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

    
48

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

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

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

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

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

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

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

    
78

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

82
  """
83
  def __init__(self, cluster_name, ipv6=False):
84
    """Initializes this class.
85

86
    @type cluster_name: str
87
    @param cluster_name: name of the cluster
88
    @type ipv6: bool
89
    @param ipv6: If true, force ssh to use IPv6 addresses only
90

91
    """
92
    self.cluster_name = cluster_name
93
    self.ipv6 = ipv6
94

    
95
  def _BuildSshOptions(self, batch, ask_key, use_cluster_key,
96
                       strict_host_check, private_key=None, quiet=True):
97
    """Builds a list with needed SSH options.
98

99
    @param batch: same as ssh's batch option
100
    @param ask_key: allows ssh to ask for key confirmation; this
101
        parameter conflicts with the batch one
102
    @param use_cluster_key: if True, use the cluster name as the
103
        HostKeyAlias name
104
    @param strict_host_check: this makes the host key checking strict
105
    @param private_key: use this private key instead of the default
106
    @param quiet: whether to enable -q to ssh
107

108
    @rtype: list
109
    @return: the list of options ready to use in L{utils.RunCmd}
110

111
    """
112
    options = [
113
      "-oEscapeChar=none",
114
      "-oHashKnownHosts=no",
115
      "-oGlobalKnownHostsFile=%s" % constants.SSH_KNOWN_HOSTS_FILE,
116
      "-oUserKnownHostsFile=/dev/null",
117
      "-oCheckHostIp=no",
118
      ]
119

    
120
    if use_cluster_key:
121
      options.append("-oHostKeyAlias=%s" % self.cluster_name)
122

    
123
    if quiet:
124
      options.append("-q")
125

    
126
    if private_key:
127
      options.append("-i%s" % private_key)
128

    
129
    # TODO: Too many boolean options, maybe convert them to more descriptive
130
    # constants.
131

    
132
    # Note: ask_key conflicts with batch mode
133
    if batch:
134
      if ask_key:
135
        raise errors.ProgrammerError("SSH call requested conflicting options")
136

    
137
      options.append("-oBatchMode=yes")
138

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

    
144
    else:
145
      # non-batch mode
146

    
147
      if ask_key:
148
        options.append("-oStrictHostKeyChecking=ask")
149
      elif strict_host_check:
150
        options.append("-oStrictHostKeyChecking=yes")
151
      else:
152
        options.append("-oStrictHostKeyChecking=no")
153

    
154
    if self.ipv6:
155
      options.append("-6")
156

    
157
    return options
158

    
159
  def BuildCmd(self, hostname, user, command, batch=True, ask_key=False,
160
               tty=False, use_cluster_key=True, strict_host_check=True,
161
               private_key=None, quiet=True):
162
    """Build an ssh command to execute a command on a remote node.
163

164
    @param hostname: the target host, string
165
    @param user: user to auth as
166
    @param command: the command
167
    @param batch: if true, ssh will run in batch mode with no prompting
168
    @param ask_key: if true, ssh will run with
169
        StrictHostKeyChecking=ask, so that we can connect to an
170
        unknown host (not valid in batch mode)
171
    @param use_cluster_key: whether to expect and use the
172
        cluster-global SSH key
173
    @param strict_host_check: whether to check the host's SSH key at all
174
    @param private_key: use this private key instead of the default
175
    @param quiet: whether to enable -q to ssh
176

177
    @return: the ssh call to run 'command' on the remote host.
178

179
    """
180
    argv = [constants.SSH]
181
    argv.extend(self._BuildSshOptions(batch, ask_key, use_cluster_key,
182
                                      strict_host_check, private_key,
183
                                      quiet=quiet))
184
    if tty:
185
      argv.extend(["-t", "-t"])
186
    argv.extend(["%s@%s" % (user, hostname), command])
187
    return argv
188

    
189
  def Run(self, *args, **kwargs):
190
    """Runs a command on a remote node.
191

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

195
    Args: see SshRunner.BuildCmd.
196

197
    @rtype: L{utils.RunResult}
198
    @return: the result as from L{utils.RunCmd()}
199

200
    """
201
    return utils.RunCmd(self.BuildCmd(*args, **kwargs))
202

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

206
    @param node: node in the cluster
207
    @param filename: absolute pathname of a local file
208

209
    @rtype: boolean
210
    @return: the success of the operation
211

212
    """
213
    if not os.path.isabs(filename):
214
      logging.error("File %s must be an absolute path", filename)
215
      return False
216

    
217
    if not os.path.isfile(filename):
218
      logging.error("File %s does not exist", filename)
219
      return False
220

    
221
    command = [constants.SCP, "-p"]
222
    command.extend(self._BuildSshOptions(True, False, True, True))
223
    command.append(filename)
224
    if netutils.IP6Address.IsValid(node):
225
      node = netutils.FormatAddress((node, None))
226

    
227
    command.append("%s:%s" % (node, filename))
228

    
229
    result = utils.RunCmd(command)
230

    
231
    if result.failed:
232
      logging.error("Copy to node %s failed (%s) error %s,"
233
                    " command was %s",
234
                    node, result.fail_reason, result.output, result.cmd)
235

    
236
    return not result.failed
237

    
238
  def VerifyNodeHostname(self, node):
239
    """Verify hostname consistency via SSH.
240

241
    This functions connects via ssh to a node and compares the hostname
242
    reported by the node to the name with have (the one that we
243
    connected to).
244

245
    This is used to detect problems in ssh known_hosts files
246
    (conflicting known hosts) and inconsistencies between dns/hosts
247
    entries and local machine names
248

249
    @param node: nodename of a host to check; can be short or
250
        full qualified hostname
251

252
    @return: (success, detail), where:
253
        - success: True/False
254
        - detail: string with details
255

256
    """
257
    retval = self.Run(node, 'root', 'hostname --fqdn')
258

    
259
    if retval.failed:
260
      msg = "ssh problem"
261
      output = retval.output
262
      if output:
263
        msg += ": %s" % output
264
      else:
265
        msg += ": %s (no output)" % retval.fail_reason
266
      logging.error("Command %s failed: %s", retval.cmd, msg)
267
      return False, msg
268

    
269
    remotehostname = retval.stdout.strip()
270

    
271
    if not remotehostname or remotehostname != node:
272
      if node.startswith(remotehostname + "."):
273
        msg = "hostname not FQDN"
274
      else:
275
        msg = "hostname mismatch"
276
      return False, ("%s: expected %s but got %s" %
277
                     (msg, node, remotehostname))
278

    
279
    return True, "host matches"
280

    
281

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

285
  """
286
  utils.WriteFile(file_name, mode=0600,
287
                  data="%s ssh-rsa %s\n" % (cfg.GetClusterName(),
288
                                            cfg.GetHostKey()))