Statistics
| Branch: | Tag: | Revision:

root / lib / ssh.py @ b43dcc5a

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

    
30
from ganeti import utils
31
from ganeti import errors
32
from ganeti import constants
33

    
34

    
35
def GetUserFiles(user, mkdir=False):
36
  """Return the paths of a user's ssh files.
37

38
  The function will return a triplet (priv_key_path, pub_key_path,
39
  auth_key_path) that are used for ssh authentication. Currently, the
40
  keys used are DSA keys, so this function will return:
41
  (~user/.ssh/id_dsa, ~user/.ssh/id_dsa.pub,
42
  ~user/.ssh/authorized_keys).
43

44
  If the optional parameter mkdir is True, the ssh directory will be
45
  created if it doesn't exist.
46

47
  Regardless of the mkdir parameters, the script will raise an error
48
  if ~user/.ssh is not a directory.
49

50
  """
51
  user_dir = utils.GetHomeDir(user)
52
  if not user_dir:
53
    raise errors.OpExecError("Cannot resolve home of user %s" % user)
54

    
55
  ssh_dir = utils.PathJoin(user_dir, ".ssh")
56
  if mkdir:
57
    utils.EnsureDirs([(ssh_dir, constants.SECURE_DIR_MODE)])
58
  elif not os.path.isdir(ssh_dir):
59
    raise errors.OpExecError("Path %s is not a directory" % ssh_dir)
60

    
61
  return [utils.PathJoin(ssh_dir, base)
62
          for base in ["id_dsa", "id_dsa.pub", "authorized_keys"]]
63

    
64

    
65
class SshRunner:
66
  """Wrapper for SSH commands.
67

68
  """
69
  def __init__(self, cluster_name, ipv6=False):
70
    """Initializes this class.
71

72
    @type cluster_name: str
73
    @param cluster_name: name of the cluster
74
    @type ipv6: bool
75
    @param ipv6: If true, force ssh to use IPv6 addresses only
76

77
    """
78
    self.cluster_name = cluster_name
79
    self.ipv6 = ipv6
80

    
81
  def _BuildSshOptions(self, batch, ask_key, use_cluster_key,
82
                       strict_host_check, private_key=None, quiet=True):
83
    """Builds a list with needed SSH options.
84

85
    @param batch: same as ssh's batch option
86
    @param ask_key: allows ssh to ask for key confirmation; this
87
        parameter conflicts with the batch one
88
    @param use_cluster_key: if True, use the cluster name as the
89
        HostKeyAlias name
90
    @param strict_host_check: this makes the host key checking strict
91
    @param private_key: use this private key instead of the default
92
    @param quiet: whether to enable -q to ssh
93

94
    @rtype: list
95
    @return: the list of options ready to use in L{utils.RunCmd}
96

97
    """
98
    options = [
99
      "-oEscapeChar=none",
100
      "-oHashKnownHosts=no",
101
      "-oGlobalKnownHostsFile=%s" % constants.SSH_KNOWN_HOSTS_FILE,
102
      "-oUserKnownHostsFile=/dev/null",
103
      "-oCheckHostIp=no",
104
      ]
105

    
106
    if use_cluster_key:
107
      options.append("-oHostKeyAlias=%s" % self.cluster_name)
108

    
109
    if quiet:
110
      options.append("-q")
111

    
112
    if private_key:
113
      options.append("-i%s" % private_key)
114

    
115
    # TODO: Too many boolean options, maybe convert them to more descriptive
116
    # constants.
117

    
118
    # Note: ask_key conflicts with batch mode
119
    if batch:
120
      if ask_key:
121
        raise errors.ProgrammerError("SSH call requested conflicting options")
122

    
123
      options.append("-oBatchMode=yes")
124

    
125
      if strict_host_check:
126
        options.append("-oStrictHostKeyChecking=yes")
127
      else:
128
        options.append("-oStrictHostKeyChecking=no")
129

    
130
    else:
131
      # non-batch mode
132

    
133
      if ask_key:
134
        options.append("-oStrictHostKeyChecking=ask")
135
      elif strict_host_check:
136
        options.append("-oStrictHostKeyChecking=yes")
137
      else:
138
        options.append("-oStrictHostKeyChecking=no")
139

    
140
    if self.ipv6:
141
      options.append("-6")
142

    
143
    return options
144

    
145
  def BuildCmd(self, hostname, user, command, batch=True, ask_key=False,
146
               tty=False, use_cluster_key=True, strict_host_check=True,
147
               private_key=None, quiet=True):
148
    """Build an ssh command to execute a command on a remote node.
149

150
    @param hostname: the target host, string
151
    @param user: user to auth as
152
    @param command: the command
153
    @param batch: if true, ssh will run in batch mode with no prompting
154
    @param ask_key: if true, ssh will run with
155
        StrictHostKeyChecking=ask, so that we can connect to an
156
        unknown host (not valid in batch mode)
157
    @param use_cluster_key: whether to expect and use the
158
        cluster-global SSH key
159
    @param strict_host_check: whether to check the host's SSH key at all
160
    @param private_key: use this private key instead of the default
161
    @param quiet: whether to enable -q to ssh
162

163
    @return: the ssh call to run 'command' on the remote host.
164

165
    """
166
    argv = [constants.SSH]
167
    argv.extend(self._BuildSshOptions(batch, ask_key, use_cluster_key,
168
                                      strict_host_check, private_key,
169
                                      quiet=quiet))
170
    if tty:
171
      argv.extend(["-t", "-t"])
172
    argv.extend(["%s@%s" % (user, hostname), command])
173
    return argv
174

    
175
  def Run(self, *args, **kwargs):
176
    """Runs a command on a remote node.
177

178
    This method has the same return value as `utils.RunCmd()`, which it
179
    uses to launch ssh.
180

181
    Args: see SshRunner.BuildCmd.
182

183
    @rtype: L{utils.RunResult}
184
    @return: the result as from L{utils.RunCmd()}
185

186
    """
187
    return utils.RunCmd(self.BuildCmd(*args, **kwargs))
188

    
189
  def CopyFileToNode(self, node, filename):
190
    """Copy a file to another node with scp.
191

192
    @param node: node in the cluster
193
    @param filename: absolute pathname of a local file
194

195
    @rtype: boolean
196
    @return: the success of the operation
197

198
    """
199
    if not os.path.isabs(filename):
200
      logging.error("File %s must be an absolute path", filename)
201
      return False
202

    
203
    if not os.path.isfile(filename):
204
      logging.error("File %s does not exist", filename)
205
      return False
206

    
207
    command = [constants.SCP, "-p"]
208
    command.extend(self._BuildSshOptions(True, False, True, True))
209
    command.append(filename)
210
    command.append("%s:%s" % (node, filename))
211

    
212
    result = utils.RunCmd(command)
213

    
214
    if result.failed:
215
      logging.error("Copy to node %s failed (%s) error %s,"
216
                    " command was %s",
217
                    node, result.fail_reason, result.output, result.cmd)
218

    
219
    return not result.failed
220

    
221
  def VerifyNodeHostname(self, node):
222
    """Verify hostname consistency via SSH.
223

224
    This functions connects via ssh to a node and compares the hostname
225
    reported by the node to the name with have (the one that we
226
    connected to).
227

228
    This is used to detect problems in ssh known_hosts files
229
    (conflicting known hosts) and inconsistencies between dns/hosts
230
    entries and local machine names
231

232
    @param node: nodename of a host to check; can be short or
233
        full qualified hostname
234

235
    @return: (success, detail), where:
236
        - success: True/False
237
        - detail: string with details
238

239
    """
240
    retval = self.Run(node, 'root', 'hostname --fqdn')
241

    
242
    if retval.failed:
243
      msg = "ssh problem"
244
      output = retval.output
245
      if output:
246
        msg += ": %s" % output
247
      else:
248
        msg += ": %s (no output)" % retval.fail_reason
249
      logging.error("Command %s failed: %s", retval.cmd, msg)
250
      return False, msg
251

    
252
    remotehostname = retval.stdout.strip()
253

    
254
    if not remotehostname or remotehostname != node:
255
      if node.startswith(remotehostname + "."):
256
        msg = "hostname not FQDN"
257
      else:
258
        msg = "hostname mistmatch"
259
      return False, ("%s: expected %s but got %s" %
260
                     (msg, node, remotehostname))
261

    
262
    return True, "host matches"
263

    
264

    
265
def WriteKnownHostsFile(cfg, file_name):
266
  """Writes the cluster-wide equally known_hosts file.
267

268
  """
269
  utils.WriteFile(file_name, mode=0600,
270
                  data="%s ssh-rsa %s\n" % (cfg.GetClusterName(),
271
                                            cfg.GetHostKey()))