Statistics
| Branch: | Tag: | Revision:

root / lib / ssh.py @ 5484cda5

History | View | Annotate | Download (10.5 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
from ganeti import compat
38

    
39

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

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

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

    
51

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

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

73
  """
74
  if _homedir_fn is None:
75
    _homedir_fn = utils.GetHomeDir
76

    
77
  user_dir = _homedir_fn(user)
78
  if not user_dir:
79
    raise errors.OpExecError("Cannot resolve home of user '%s'" % user)
80

    
81
  if kind == constants.SSHK_DSA:
82
    suffix = "dsa"
83
  elif kind == constants.SSHK_RSA:
84
    suffix = "rsa"
85
  else:
86
    raise errors.ProgrammerError("Unknown SSH key kind '%s'" % kind)
87

    
88
  ssh_dir = utils.PathJoin(user_dir, ".ssh")
89
  if mkdir:
90
    utils.EnsureDirs([(ssh_dir, constants.SECURE_DIR_MODE)])
91
  elif dircheck and not os.path.isdir(ssh_dir):
92
    raise errors.OpExecError("Path %s is not a directory" % ssh_dir)
93

    
94
  return [utils.PathJoin(ssh_dir, base)
95
          for base in ["id_%s" % suffix, "id_%s.pub" % suffix,
96
                       "authorized_keys"]]
97

    
98

    
99
def GetAllUserFiles(user, mkdir=False, dircheck=True, _homedir_fn=None):
100
  """Wrapper over L{GetUserFiles} to retrieve files for all SSH key types.
101

102
  See L{GetUserFiles} for details.
103

104
  @rtype: tuple; (string, dict with string as key, tuple of (string, string) as
105
    value)
106

107
  """
108
  helper = compat.partial(GetUserFiles, user, mkdir=mkdir, dircheck=dircheck,
109
                          _homedir_fn=_homedir_fn)
110
  result = [(kind, helper(kind=kind)) for kind in constants.SSHK_ALL]
111

    
112
  authorized_keys = [i for (_, (_, _, i)) in result]
113

    
114
  assert len(frozenset(authorized_keys)) == 1, \
115
    "Different paths for authorized_keys were returned"
116

    
117
  return (authorized_keys[0],
118
          dict((kind, (privkey, pubkey))
119
               for (kind, (privkey, pubkey, _)) in result))
120

    
121

    
122
class SshRunner:
123
  """Wrapper for SSH commands.
124

125
  """
126
  def __init__(self, cluster_name, ipv6=False):
127
    """Initializes this class.
128

129
    @type cluster_name: str
130
    @param cluster_name: name of the cluster
131
    @type ipv6: bool
132
    @param ipv6: If true, force ssh to use IPv6 addresses only
133

134
    """
135
    self.cluster_name = cluster_name
136
    self.ipv6 = ipv6
137

    
138
  def _BuildSshOptions(self, batch, ask_key, use_cluster_key,
139
                       strict_host_check, private_key=None, quiet=True):
140
    """Builds a list with needed SSH options.
141

142
    @param batch: same as ssh's batch option
143
    @param ask_key: allows ssh to ask for key confirmation; this
144
        parameter conflicts with the batch one
145
    @param use_cluster_key: if True, use the cluster name as the
146
        HostKeyAlias name
147
    @param strict_host_check: this makes the host key checking strict
148
    @param private_key: use this private key instead of the default
149
    @param quiet: whether to enable -q to ssh
150

151
    @rtype: list
152
    @return: the list of options ready to use in L{utils.process.RunCmd}
153

154
    """
155
    options = [
156
      "-oEscapeChar=none",
157
      "-oHashKnownHosts=no",
158
      "-oGlobalKnownHostsFile=%s" % pathutils.SSH_KNOWN_HOSTS_FILE,
159
      "-oUserKnownHostsFile=/dev/null",
160
      "-oCheckHostIp=no",
161
      ]
162

    
163
    if use_cluster_key:
164
      options.append("-oHostKeyAlias=%s" % self.cluster_name)
165

    
166
    if quiet:
167
      options.append("-q")
168

    
169
    if private_key:
170
      options.append("-i%s" % private_key)
171

    
172
    # TODO: Too many boolean options, maybe convert them to more descriptive
173
    # constants.
174

    
175
    # Note: ask_key conflicts with batch mode
176
    if batch:
177
      if ask_key:
178
        raise errors.ProgrammerError("SSH call requested conflicting options")
179

    
180
      options.append("-oBatchMode=yes")
181

    
182
      if strict_host_check:
183
        options.append("-oStrictHostKeyChecking=yes")
184
      else:
185
        options.append("-oStrictHostKeyChecking=no")
186

    
187
    else:
188
      # non-batch mode
189

    
190
      if ask_key:
191
        options.append("-oStrictHostKeyChecking=ask")
192
      elif strict_host_check:
193
        options.append("-oStrictHostKeyChecking=yes")
194
      else:
195
        options.append("-oStrictHostKeyChecking=no")
196

    
197
    if self.ipv6:
198
      options.append("-6")
199

    
200
    return options
201

    
202
  def BuildCmd(self, hostname, user, command, batch=True, ask_key=False,
203
               tty=False, use_cluster_key=True, strict_host_check=True,
204
               private_key=None, quiet=True):
205
    """Build an ssh command to execute a command on a remote node.
206

207
    @param hostname: the target host, string
208
    @param user: user to auth as
209
    @param command: the command
210
    @param batch: if true, ssh will run in batch mode with no prompting
211
    @param ask_key: if true, ssh will run with
212
        StrictHostKeyChecking=ask, so that we can connect to an
213
        unknown host (not valid in batch mode)
214
    @param use_cluster_key: whether to expect and use the
215
        cluster-global SSH key
216
    @param strict_host_check: whether to check the host's SSH key at all
217
    @param private_key: use this private key instead of the default
218
    @param quiet: whether to enable -q to ssh
219

220
    @return: the ssh call to run 'command' on the remote host.
221

222
    """
223
    argv = [constants.SSH]
224
    argv.extend(self._BuildSshOptions(batch, ask_key, use_cluster_key,
225
                                      strict_host_check, private_key,
226
                                      quiet=quiet))
227
    if tty:
228
      argv.extend(["-t", "-t"])
229

    
230
    argv.append("%s@%s" % (user, hostname))
231

    
232
    # Insert variables for virtual nodes
233
    argv.extend("export %s=%s;" %
234
                (utils.ShellQuote(name), utils.ShellQuote(value))
235
                for (name, value) in
236
                  vcluster.EnvironmentForHost(hostname).items())
237

    
238
    argv.append(command)
239

    
240
    return argv
241

    
242
  def Run(self, *args, **kwargs):
243
    """Runs a command on a remote node.
244

245
    This method has the same return value as `utils.RunCmd()`, which it
246
    uses to launch ssh.
247

248
    Args: see SshRunner.BuildCmd.
249

250
    @rtype: L{utils.process.RunResult}
251
    @return: the result as from L{utils.process.RunCmd()}
252

253
    """
254
    return utils.RunCmd(self.BuildCmd(*args, **kwargs))
255

    
256
  def CopyFileToNode(self, node, filename):
257
    """Copy a file to another node with scp.
258

259
    @param node: node in the cluster
260
    @param filename: absolute pathname of a local file
261

262
    @rtype: boolean
263
    @return: the success of the operation
264

265
    """
266
    if not os.path.isabs(filename):
267
      logging.error("File %s must be an absolute path", filename)
268
      return False
269

    
270
    if not os.path.isfile(filename):
271
      logging.error("File %s does not exist", filename)
272
      return False
273

    
274
    command = [constants.SCP, "-p"]
275
    command.extend(self._BuildSshOptions(True, False, True, True))
276
    command.append(filename)
277
    if netutils.IP6Address.IsValid(node):
278
      node = netutils.FormatAddress((node, None))
279

    
280
    command.append("%s:%s" % (node, vcluster.ExchangeNodeRoot(node, filename)))
281

    
282
    result = utils.RunCmd(command)
283

    
284
    if result.failed:
285
      logging.error("Copy to node %s failed (%s) error '%s',"
286
                    " command was '%s'",
287
                    node, result.fail_reason, result.output, result.cmd)
288

    
289
    return not result.failed
290

    
291
  def VerifyNodeHostname(self, node):
292
    """Verify hostname consistency via SSH.
293

294
    This functions connects via ssh to a node and compares the hostname
295
    reported by the node to the name with have (the one that we
296
    connected to).
297

298
    This is used to detect problems in ssh known_hosts files
299
    (conflicting known hosts) and inconsistencies between dns/hosts
300
    entries and local machine names
301

302
    @param node: nodename of a host to check; can be short or
303
        full qualified hostname
304

305
    @return: (success, detail), where:
306
        - success: True/False
307
        - detail: string with details
308

309
    """
310
    cmd = ("if test -z \"$GANETI_HOSTNAME\"; then"
311
           "  hostname --fqdn;"
312
           "else"
313
           "  echo \"$GANETI_HOSTNAME\";"
314
           "fi")
315
    retval = self.Run(node, constants.SSH_LOGIN_USER, cmd, quiet=False)
316

    
317
    if retval.failed:
318
      msg = "ssh problem"
319
      output = retval.output
320
      if output:
321
        msg += ": %s" % output
322
      else:
323
        msg += ": %s (no output)" % retval.fail_reason
324
      logging.error("Command %s failed: %s", retval.cmd, msg)
325
      return False, msg
326

    
327
    remotehostname = retval.stdout.strip()
328

    
329
    if not remotehostname or remotehostname != node:
330
      if node.startswith(remotehostname + "."):
331
        msg = "hostname not FQDN"
332
      else:
333
        msg = "hostname mismatch"
334
      return False, ("%s: expected %s but got %s" %
335
                     (msg, node, remotehostname))
336

    
337
    return True, "host matches"
338

    
339

    
340
def WriteKnownHostsFile(cfg, file_name):
341
  """Writes the cluster-wide equally known_hosts file.
342

343
  """
344
  utils.WriteFile(file_name, mode=0600,
345
                  data="%s ssh-rsa %s\n" % (cfg.GetClusterName(),
346
                                            cfg.GetHostKey()))