Statistics
| Branch: | Tag: | Revision:

root / lib / ssh.py @ 7bd70e6b

History | View | Annotate | Download (9.7 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, dircheck=True, 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 dircheck: bool
60
  @param dircheck: Whether to check if ".ssh" directory exists
61
  @type kind: string
62
  @param kind: One of L{constants.SSHK_ALL}
63
  @rtype: tuple; (string, string, string)
64
  @return: Tuple containing three file system paths; the private SSH key file,
65
    the public SSH key file and the user's C{authorized_keys} file
66
  @raise errors.OpExecError: When home directory of the user can not be
67
    determined
68
  @raise errors.OpExecError: Regardless of the C{mkdir} parameters, this
69
    exception is raised if C{~$user/.ssh} is not a directory and C{dircheck}
70
    is set to C{True}
71

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

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

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

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

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

    
97

    
98
class SshRunner:
99
  """Wrapper for SSH commands.
100

101
  """
102
  def __init__(self, cluster_name, ipv6=False):
103
    """Initializes this class.
104

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

110
    """
111
    self.cluster_name = cluster_name
112
    self.ipv6 = ipv6
113

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

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

127
    @rtype: list
128
    @return: the list of options ready to use in L{utils.process.RunCmd}
129

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

    
139
    if use_cluster_key:
140
      options.append("-oHostKeyAlias=%s" % self.cluster_name)
141

    
142
    if quiet:
143
      options.append("-q")
144

    
145
    if private_key:
146
      options.append("-i%s" % private_key)
147

    
148
    # TODO: Too many boolean options, maybe convert them to more descriptive
149
    # constants.
150

    
151
    # Note: ask_key conflicts with batch mode
152
    if batch:
153
      if ask_key:
154
        raise errors.ProgrammerError("SSH call requested conflicting options")
155

    
156
      options.append("-oBatchMode=yes")
157

    
158
      if strict_host_check:
159
        options.append("-oStrictHostKeyChecking=yes")
160
      else:
161
        options.append("-oStrictHostKeyChecking=no")
162

    
163
    else:
164
      # non-batch mode
165

    
166
      if ask_key:
167
        options.append("-oStrictHostKeyChecking=ask")
168
      elif strict_host_check:
169
        options.append("-oStrictHostKeyChecking=yes")
170
      else:
171
        options.append("-oStrictHostKeyChecking=no")
172

    
173
    if self.ipv6:
174
      options.append("-6")
175

    
176
    return options
177

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

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

196
    @return: the ssh call to run 'command' on the remote host.
197

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

    
206
    argv.append("%s@%s" % (user, hostname))
207

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

    
214
    argv.append(command)
215

    
216
    return argv
217

    
218
  def Run(self, *args, **kwargs):
219
    """Runs a command on a remote node.
220

221
    This method has the same return value as `utils.RunCmd()`, which it
222
    uses to launch ssh.
223

224
    Args: see SshRunner.BuildCmd.
225

226
    @rtype: L{utils.process.RunResult}
227
    @return: the result as from L{utils.process.RunCmd()}
228

229
    """
230
    return utils.RunCmd(self.BuildCmd(*args, **kwargs))
231

    
232
  def CopyFileToNode(self, node, filename):
233
    """Copy a file to another node with scp.
234

235
    @param node: node in the cluster
236
    @param filename: absolute pathname of a local file
237

238
    @rtype: boolean
239
    @return: the success of the operation
240

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

    
246
    if not os.path.isfile(filename):
247
      logging.error("File %s does not exist", filename)
248
      return False
249

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

    
256
    command.append("%s:%s" % (node, vcluster.ExchangeNodeRoot(node, filename)))
257

    
258
    result = utils.RunCmd(command)
259

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

    
265
    return not result.failed
266

    
267
  def VerifyNodeHostname(self, node):
268
    """Verify hostname consistency via SSH.
269

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

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

278
    @param node: nodename of a host to check; can be short or
279
        full qualified hostname
280

281
    @return: (success, detail), where:
282
        - success: True/False
283
        - detail: string with details
284

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

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

    
303
    remotehostname = retval.stdout.strip()
304

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

    
313
    return True, "host matches"
314

    
315

    
316
def WriteKnownHostsFile(cfg, file_name):
317
  """Writes the cluster-wide equally known_hosts file.
318

319
  """
320
  utils.WriteFile(file_name, mode=0600,
321
                  data="%s ssh-rsa %s\n" % (cfg.GetClusterName(),
322
                                            cfg.GetHostKey()))