Statistics
| Branch: | Tag: | Revision:

root / lib / ssh.py @ bc57fa8d

History | View | Annotate | Download (10.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

    
30
from ganeti import utils
31
from ganeti import errors
32
from ganeti import constants
33
from ganeti import netutils
34
from ganeti import pathutils
35
from ganeti import vcluster
36
from ganeti import compat
37

    
38

    
39
def GetUserFiles(user, mkdir=False, dircheck=True, kind=constants.SSHK_DSA,
40
                 _homedir_fn=None):
41
  """Return the paths of a user's SSH files.
42

43
  @type user: string
44
  @param user: Username
45
  @type mkdir: bool
46
  @param mkdir: Whether to create ".ssh" directory if it doesn't exist
47
  @type dircheck: bool
48
  @param dircheck: Whether to check if ".ssh" directory exists
49
  @type kind: string
50
  @param kind: One of L{constants.SSHK_ALL}
51
  @rtype: tuple; (string, string, string)
52
  @return: Tuple containing three file system paths; the private SSH key file,
53
    the public SSH key file and the user's C{authorized_keys} file
54
  @raise errors.OpExecError: When home directory of the user can not be
55
    determined
56
  @raise errors.OpExecError: Regardless of the C{mkdir} parameters, this
57
    exception is raised if C{~$user/.ssh} is not a directory and C{dircheck}
58
    is set to C{True}
59

60
  """
61
  if _homedir_fn is None:
62
    _homedir_fn = utils.GetHomeDir
63

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

    
68
  if kind == constants.SSHK_DSA:
69
    suffix = "dsa"
70
  elif kind == constants.SSHK_RSA:
71
    suffix = "rsa"
72
  else:
73
    raise errors.ProgrammerError("Unknown SSH key kind '%s'" % kind)
74

    
75
  ssh_dir = utils.PathJoin(user_dir, ".ssh")
76
  if mkdir:
77
    utils.EnsureDirs([(ssh_dir, constants.SECURE_DIR_MODE)])
78
  elif dircheck and not os.path.isdir(ssh_dir):
79
    raise errors.OpExecError("Path %s is not a directory" % ssh_dir)
80

    
81
  return [utils.PathJoin(ssh_dir, base)
82
          for base in ["id_%s" % suffix, "id_%s.pub" % suffix,
83
                       "authorized_keys"]]
84

    
85

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

89
  See L{GetUserFiles} for details.
90

91
  @rtype: tuple; (string, dict with string as key, tuple of (string, string) as
92
    value)
93

94
  """
95
  helper = compat.partial(GetUserFiles, user, mkdir=mkdir, dircheck=dircheck,
96
                          _homedir_fn=_homedir_fn)
97
  result = [(kind, helper(kind=kind)) for kind in constants.SSHK_ALL]
98

    
99
  authorized_keys = [i for (_, (_, _, i)) in result]
100

    
101
  assert len(frozenset(authorized_keys)) == 1, \
102
    "Different paths for authorized_keys were returned"
103

    
104
  return (authorized_keys[0],
105
          dict((kind, (privkey, pubkey))
106
               for (kind, (privkey, pubkey, _)) in result))
107

    
108

    
109
class SshRunner:
110
  """Wrapper for SSH commands.
111

112
  """
113
  def __init__(self, cluster_name, ipv6=False):
114
    """Initializes this class.
115

116
    @type cluster_name: str
117
    @param cluster_name: name of the cluster
118
    @type ipv6: bool
119
    @param ipv6: If true, force ssh to use IPv6 addresses only
120

121
    """
122
    self.cluster_name = cluster_name
123
    self.ipv6 = ipv6
124

    
125
  def _BuildSshOptions(self, batch, ask_key, use_cluster_key,
126
                       strict_host_check, private_key=None, quiet=True,
127
                       port=None):
128
    """Builds a list with needed SSH options.
129

130
    @param batch: same as ssh's batch option
131
    @param ask_key: allows ssh to ask for key confirmation; this
132
        parameter conflicts with the batch one
133
    @param use_cluster_key: if True, use the cluster name as the
134
        HostKeyAlias name
135
    @param strict_host_check: this makes the host key checking strict
136
    @param private_key: use this private key instead of the default
137
    @param quiet: whether to enable -q to ssh
138
    @param port: the SSH port to use, or None to use the default
139

140
    @rtype: list
141
    @return: the list of options ready to use in L{utils.process.RunCmd}
142

143
    """
144
    options = [
145
      "-oEscapeChar=none",
146
      "-oHashKnownHosts=no",
147
      "-oGlobalKnownHostsFile=%s" % pathutils.SSH_KNOWN_HOSTS_FILE,
148
      "-oUserKnownHostsFile=/dev/null",
149
      "-oCheckHostIp=no",
150
      ]
151

    
152
    if use_cluster_key:
153
      options.append("-oHostKeyAlias=%s" % self.cluster_name)
154

    
155
    if quiet:
156
      options.append("-q")
157

    
158
    if private_key:
159
      options.append("-i%s" % private_key)
160

    
161
    if port:
162
      options.append("-oPort=%d" % port)
163

    
164
    # TODO: Too many boolean options, maybe convert them to more descriptive
165
    # constants.
166

    
167
    # Note: ask_key conflicts with batch mode
168
    if batch:
169
      if ask_key:
170
        raise errors.ProgrammerError("SSH call requested conflicting options")
171

    
172
      options.append("-oBatchMode=yes")
173

    
174
      if strict_host_check:
175
        options.append("-oStrictHostKeyChecking=yes")
176
      else:
177
        options.append("-oStrictHostKeyChecking=no")
178

    
179
    else:
180
      # non-batch mode
181

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

    
189
    if self.ipv6:
190
      options.append("-6")
191
    else:
192
      options.append("-4")
193

    
194
    return options
195

    
196
  def BuildCmd(self, hostname, user, command, batch=True, ask_key=False,
197
               tty=False, use_cluster_key=True, strict_host_check=True,
198
               private_key=None, quiet=True, port=None):
199
    """Build an ssh command to execute a command on a remote node.
200

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

215
    @return: the ssh call to run 'command' on the remote host.
216

217
    """
218
    argv = [constants.SSH]
219
    argv.extend(self._BuildSshOptions(batch, ask_key, use_cluster_key,
220
                                      strict_host_check, private_key,
221
                                      quiet=quiet, port=port))
222
    if tty:
223
      argv.extend(["-t", "-t"])
224

    
225
    argv.append("%s@%s" % (user, hostname))
226

    
227
    # Insert variables for virtual nodes
228
    argv.extend("export %s=%s;" %
229
                (utils.ShellQuote(name), utils.ShellQuote(value))
230
                for (name, value) in
231
                  vcluster.EnvironmentForHost(hostname).items())
232

    
233
    argv.append(command)
234

    
235
    return argv
236

    
237
  def Run(self, *args, **kwargs):
238
    """Runs a command on a remote node.
239

240
    This method has the same return value as `utils.RunCmd()`, which it
241
    uses to launch ssh.
242

243
    Args: see SshRunner.BuildCmd.
244

245
    @rtype: L{utils.process.RunResult}
246
    @return: the result as from L{utils.process.RunCmd()}
247

248
    """
249
    return utils.RunCmd(self.BuildCmd(*args, **kwargs))
250

    
251
  def CopyFileToNode(self, node, port, filename):
252
    """Copy a file to another node with scp.
253

254
    @param node: node in the cluster
255
    @param filename: absolute pathname of a local file
256

257
    @rtype: boolean
258
    @return: the success of the operation
259

260
    """
261
    if not os.path.isabs(filename):
262
      logging.error("File %s must be an absolute path", filename)
263
      return False
264

    
265
    if not os.path.isfile(filename):
266
      logging.error("File %s does not exist", filename)
267
      return False
268

    
269
    command = [constants.SCP, "-p"]
270
    command.extend(self._BuildSshOptions(True, False, True, True, port=port))
271
    command.append(filename)
272
    if netutils.IP6Address.IsValid(node):
273
      node = netutils.FormatAddress((node, None))
274

    
275
    command.append("%s:%s" % (node, vcluster.ExchangeNodeRoot(node, filename)))
276

    
277
    result = utils.RunCmd(command)
278

    
279
    if result.failed:
280
      logging.error("Copy to node %s failed (%s) error '%s',"
281
                    " command was '%s'",
282
                    node, result.fail_reason, result.output, result.cmd)
283

    
284
    return not result.failed
285

    
286
  def VerifyNodeHostname(self, node, ssh_port):
287
    """Verify hostname consistency via SSH.
288

289
    This functions connects via ssh to a node and compares the hostname
290
    reported by the node to the name with have (the one that we
291
    connected to).
292

293
    This is used to detect problems in ssh known_hosts files
294
    (conflicting known hosts) and inconsistencies between dns/hosts
295
    entries and local machine names
296

297
    @param node: nodename of a host to check; can be short or
298
        full qualified hostname
299
    @param ssh_port: the port of a SSH daemon running on the node
300

301
    @return: (success, detail), where:
302
        - success: True/False
303
        - detail: string with details
304

305
    """
306
    cmd = ("if test -z \"$GANETI_HOSTNAME\"; then"
307
           "  hostname --fqdn;"
308
           "else"
309
           "  echo \"$GANETI_HOSTNAME\";"
310
           "fi")
311
    retval = self.Run(node, constants.SSH_LOGIN_USER, cmd,
312
                      quiet=False, port=ssh_port)
313

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

    
324
    remotehostname = retval.stdout.strip()
325

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

    
334
    return True, "host matches"
335

    
336

    
337
def WriteKnownHostsFile(cfg, file_name):
338
  """Writes the cluster-wide equally known_hosts file.
339

340
  """
341
  data = ""
342
  if cfg.GetRsaHostKey():
343
    data += "%s ssh-rsa %s\n" % (cfg.GetClusterName(), cfg.GetRsaHostKey())
344
  if cfg.GetDsaHostKey():
345
    data += "%s ssh-dss %s\n" % (cfg.GetClusterName(), cfg.GetDsaHostKey())
346

    
347
  utils.WriteFile(file_name, mode=0600, data=data)