Statistics
| Branch: | Tag: | Revision:

root / lib / ssh.py @ 9294514d

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

    
35

    
36
def FormatParamikoFingerprint(fingerprint):
37
  """Formats the fingerprint of L{paramiko.PKey.get_fingerprint()}
38

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

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

    
47

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

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

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

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

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

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

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

    
77

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
143
    else:
144
      # non-batch mode
145

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

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

    
156
    return options
157

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

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

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

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

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

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

194
    Args: see SshRunner.BuildCmd.
195

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

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

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

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

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

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

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

    
220
    command = [constants.SCP, "-p"]
221
    command.extend(self._BuildSshOptions(True, False, True, True))
222
    command.append(filename)
223
    command.append("%s:%s" % (node, filename))
224

    
225
    result = utils.RunCmd(command)
226

    
227
    if result.failed:
228
      logging.error("Copy to node %s failed (%s) error %s,"
229
                    " command was %s",
230
                    node, result.fail_reason, result.output, result.cmd)
231

    
232
    return not result.failed
233

    
234
  def VerifyNodeHostname(self, node):
235
    """Verify hostname consistency via SSH.
236

237
    This functions connects via ssh to a node and compares the hostname
238
    reported by the node to the name with have (the one that we
239
    connected to).
240

241
    This is used to detect problems in ssh known_hosts files
242
    (conflicting known hosts) and inconsistencies between dns/hosts
243
    entries and local machine names
244

245
    @param node: nodename of a host to check; can be short or
246
        full qualified hostname
247

248
    @return: (success, detail), where:
249
        - success: True/False
250
        - detail: string with details
251

252
    """
253
    retval = self.Run(node, 'root', 'hostname --fqdn')
254

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

    
265
    remotehostname = retval.stdout.strip()
266

    
267
    if not remotehostname or remotehostname != node:
268
      if node.startswith(remotehostname + "."):
269
        msg = "hostname not FQDN"
270
      else:
271
        msg = "hostname mismatch"
272
      return False, ("%s: expected %s but got %s" %
273
                     (msg, node, remotehostname))
274

    
275
    return True, "host matches"
276

    
277

    
278
def WriteKnownHostsFile(cfg, file_name):
279
  """Writes the cluster-wide equally known_hosts file.
280

281
  """
282
  utils.WriteFile(file_name, mode=0600,
283
                  data="%s ssh-rsa %s\n" % (cfg.GetClusterName(),
284
                                            cfg.GetHostKey()))