Statistics
| Branch: | Tag: | Revision:

root / lib / ssh.py @ d12b9f66

History | View | Annotate | Download (9.6 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010, 2011 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
from ganeti import pathutils
36
from ganeti import vcluster
37

    
38

    
39
def FormatParamikoFingerprint(fingerprint):
40
  """Format paramiko PKey fingerprint.
41

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

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

    
50

    
51
def GetUserFiles(user, mkdir=False, kind=constants.SSHK_DSA,
52
                 _homedir_fn=None):
53
  """Return the paths of a user's SSH files.
54

55
  @type user: string
56
  @param user: Username
57
  @type mkdir: bool
58
  @param mkdir: Whether to create ".ssh" directory if it doesn't exist
59
  @type kind: string
60
  @param kind: One of L{constants.SSHK_ALL}
61
  @rtype: tuple; (string, string, string)
62
  @return: Tuple containing three file system paths; the private SSH key file,
63
    the public SSH key file and the user's C{authorized_keys} file
64
  @raise errors.OpExecError: When home directory of the user can not be
65
    determined
66
  @raise errors.OpExecError: Regardless of the C{mkdir} parameters, this
67
    exception is raised if C{~$user/.ssh} is not a directory
68

69
  """
70
  if _homedir_fn is None:
71
    _homedir_fn = utils.GetHomeDir
72

    
73
  user_dir = _homedir_fn(user)
74
  if not user_dir:
75
    raise errors.OpExecError("Cannot resolve home of user '%s'" % user)
76

    
77
  if kind == constants.SSHK_DSA:
78
    suffix = "dsa"
79
  elif kind == constants.SSHK_RSA:
80
    suffix = "rsa"
81
  else:
82
    raise errors.ProgrammerError("Unknown SSH key kind '%s'" % kind)
83

    
84
  ssh_dir = utils.PathJoin(user_dir, ".ssh")
85
  if mkdir:
86
    utils.EnsureDirs([(ssh_dir, constants.SECURE_DIR_MODE)])
87
  elif not os.path.isdir(ssh_dir):
88
    raise errors.OpExecError("Path %s is not a directory" % ssh_dir)
89

    
90
  return [utils.PathJoin(ssh_dir, base)
91
          for base in ["id_%s" % suffix, "id_%s.pub" % suffix,
92
                       "authorized_keys"]]
93

    
94

    
95
class SshRunner:
96
  """Wrapper for SSH commands.
97

98
  """
99
  def __init__(self, cluster_name, ipv6=False):
100
    """Initializes this class.
101

102
    @type cluster_name: str
103
    @param cluster_name: name of the cluster
104
    @type ipv6: bool
105
    @param ipv6: If true, force ssh to use IPv6 addresses only
106

107
    """
108
    self.cluster_name = cluster_name
109
    self.ipv6 = ipv6
110

    
111
  def _BuildSshOptions(self, batch, ask_key, use_cluster_key,
112
                       strict_host_check, private_key=None, quiet=True):
113
    """Builds a list with needed SSH options.
114

115
    @param batch: same as ssh's batch option
116
    @param ask_key: allows ssh to ask for key confirmation; this
117
        parameter conflicts with the batch one
118
    @param use_cluster_key: if True, use the cluster name as the
119
        HostKeyAlias name
120
    @param strict_host_check: this makes the host key checking strict
121
    @param private_key: use this private key instead of the default
122
    @param quiet: whether to enable -q to ssh
123

124
    @rtype: list
125
    @return: the list of options ready to use in L{utils.process.RunCmd}
126

127
    """
128
    options = [
129
      "-oEscapeChar=none",
130
      "-oHashKnownHosts=no",
131
      "-oGlobalKnownHostsFile=%s" % pathutils.SSH_KNOWN_HOSTS_FILE,
132
      "-oUserKnownHostsFile=/dev/null",
133
      "-oCheckHostIp=no",
134
      ]
135

    
136
    if use_cluster_key:
137
      options.append("-oHostKeyAlias=%s" % self.cluster_name)
138

    
139
    if quiet:
140
      options.append("-q")
141

    
142
    if private_key:
143
      options.append("-i%s" % private_key)
144

    
145
    # TODO: Too many boolean options, maybe convert them to more descriptive
146
    # constants.
147

    
148
    # Note: ask_key conflicts with batch mode
149
    if batch:
150
      if ask_key:
151
        raise errors.ProgrammerError("SSH call requested conflicting options")
152

    
153
      options.append("-oBatchMode=yes")
154

    
155
      if strict_host_check:
156
        options.append("-oStrictHostKeyChecking=yes")
157
      else:
158
        options.append("-oStrictHostKeyChecking=no")
159

    
160
    else:
161
      # non-batch mode
162

    
163
      if ask_key:
164
        options.append("-oStrictHostKeyChecking=ask")
165
      elif strict_host_check:
166
        options.append("-oStrictHostKeyChecking=yes")
167
      else:
168
        options.append("-oStrictHostKeyChecking=no")
169

    
170
    if self.ipv6:
171
      options.append("-6")
172

    
173
    return options
174

    
175
  def BuildCmd(self, hostname, user, command, batch=True, ask_key=False,
176
               tty=False, use_cluster_key=True, strict_host_check=True,
177
               private_key=None, quiet=True):
178
    """Build an ssh command to execute a command on a remote node.
179

180
    @param hostname: the target host, string
181
    @param user: user to auth as
182
    @param command: the command
183
    @param batch: if true, ssh will run in batch mode with no prompting
184
    @param ask_key: if true, ssh will run with
185
        StrictHostKeyChecking=ask, so that we can connect to an
186
        unknown host (not valid in batch mode)
187
    @param use_cluster_key: whether to expect and use the
188
        cluster-global SSH key
189
    @param strict_host_check: whether to check the host's SSH key at all
190
    @param private_key: use this private key instead of the default
191
    @param quiet: whether to enable -q to ssh
192

193
    @return: the ssh call to run 'command' on the remote host.
194

195
    """
196
    argv = [constants.SSH]
197
    argv.extend(self._BuildSshOptions(batch, ask_key, use_cluster_key,
198
                                      strict_host_check, private_key,
199
                                      quiet=quiet))
200
    if tty:
201
      argv.extend(["-t", "-t"])
202

    
203
    argv.append("%s@%s" % (user, hostname))
204

    
205
    # Insert variables for virtual nodes
206
    argv.extend("export %s=%s;" %
207
                (utils.ShellQuote(name), utils.ShellQuote(value))
208
                for (name, value) in
209
                  vcluster.EnvironmentForHost(hostname).items())
210

    
211
    argv.append(command)
212

    
213
    return argv
214

    
215
  def Run(self, *args, **kwargs):
216
    """Runs a command on a remote node.
217

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

221
    Args: see SshRunner.BuildCmd.
222

223
    @rtype: L{utils.process.RunResult}
224
    @return: the result as from L{utils.process.RunCmd()}
225

226
    """
227
    return utils.RunCmd(self.BuildCmd(*args, **kwargs))
228

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

232
    @param node: node in the cluster
233
    @param filename: absolute pathname of a local file
234

235
    @rtype: boolean
236
    @return: the success of the operation
237

238
    """
239
    if not os.path.isabs(filename):
240
      logging.error("File %s must be an absolute path", filename)
241
      return False
242

    
243
    if not os.path.isfile(filename):
244
      logging.error("File %s does not exist", filename)
245
      return False
246

    
247
    command = [constants.SCP, "-p"]
248
    command.extend(self._BuildSshOptions(True, False, True, True))
249
    command.append(filename)
250
    if netutils.IP6Address.IsValid(node):
251
      node = netutils.FormatAddress((node, None))
252

    
253
    command.append("%s:%s" % (node, vcluster.ExchangeNodeRoot(node, filename)))
254

    
255
    result = utils.RunCmd(command)
256

    
257
    if result.failed:
258
      logging.error("Copy to node %s failed (%s) error '%s',"
259
                    " command was '%s'",
260
                    node, result.fail_reason, result.output, result.cmd)
261

    
262
    return not result.failed
263

    
264
  def VerifyNodeHostname(self, node):
265
    """Verify hostname consistency via SSH.
266

267
    This functions connects via ssh to a node and compares the hostname
268
    reported by the node to the name with have (the one that we
269
    connected to).
270

271
    This is used to detect problems in ssh known_hosts files
272
    (conflicting known hosts) and inconsistencies between dns/hosts
273
    entries and local machine names
274

275
    @param node: nodename of a host to check; can be short or
276
        full qualified hostname
277

278
    @return: (success, detail), where:
279
        - success: True/False
280
        - detail: string with details
281

282
    """
283
    cmd = ("if test -z \"$GANETI_HOSTNAME\"; then"
284
           "  hostname --fqdn;"
285
           "else"
286
           "  echo \"$GANETI_HOSTNAME\";"
287
           "fi")
288
    retval = self.Run(node, constants.SSH_LOGIN_USER, cmd, quiet=False)
289

    
290
    if retval.failed:
291
      msg = "ssh problem"
292
      output = retval.output
293
      if output:
294
        msg += ": %s" % output
295
      else:
296
        msg += ": %s (no output)" % retval.fail_reason
297
      logging.error("Command %s failed: %s", retval.cmd, msg)
298
      return False, msg
299

    
300
    remotehostname = retval.stdout.strip()
301

    
302
    if not remotehostname or remotehostname != node:
303
      if node.startswith(remotehostname + "."):
304
        msg = "hostname not FQDN"
305
      else:
306
        msg = "hostname mismatch"
307
      return False, ("%s: expected %s but got %s" %
308
                     (msg, node, remotehostname))
309

    
310
    return True, "host matches"
311

    
312

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

316
  """
317
  utils.WriteFile(file_name, mode=0600,
318
                  data="%s ssh-rsa %s\n" % (cfg.GetClusterName(),
319
                                            cfg.GetHostKey()))