Package ganeti :: Module ssh
[hide private]
[frames] | no frames]

Source Code for Module ganeti.ssh

  1  # 
  2  # 
  3   
  4  # Copyright (C) 2006, 2007, 2010, 2011 Google Inc. 
  5  # All rights reserved. 
  6  # 
  7  # Redistribution and use in source and binary forms, with or without 
  8  # modification, are permitted provided that the following conditions are 
  9  # met: 
 10  # 
 11  # 1. Redistributions of source code must retain the above copyright notice, 
 12  # this list of conditions and the following disclaimer. 
 13  # 
 14  # 2. Redistributions in binary form must reproduce the above copyright 
 15  # notice, this list of conditions and the following disclaimer in the 
 16  # documentation and/or other materials provided with the distribution. 
 17  # 
 18  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
 19  # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
 20  # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 21  # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 22  # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 23  # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 24  # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 25  # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 26  # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 27  # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 28  # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 29   
 30   
 31  """Module encapsulating ssh functionality. 
 32   
 33  """ 
 34   
 35   
 36  import os 
 37  import logging 
 38   
 39  from ganeti import utils 
 40  from ganeti import errors 
 41  from ganeti import constants 
 42  from ganeti import netutils 
 43  from ganeti import pathutils 
 44  from ganeti import vcluster 
 45  from ganeti import compat 
 46  from ganeti import ssconf 
 47   
 48   
49 -def GetUserFiles(user, mkdir=False, dircheck=True, kind=constants.SSHK_DSA, 50 _homedir_fn=None):
51 """Return the paths of a user's SSH files. 52 53 @type user: string 54 @param user: Username 55 @type mkdir: bool 56 @param mkdir: Whether to create ".ssh" directory if it doesn't exist 57 @type dircheck: bool 58 @param dircheck: Whether to check if ".ssh" directory exists 59 @type kind: string 60 @param kind: One of L{constants.SSHK_ALL} 61 @rtype: tuple; (string, string, string) 62 @return: Tuple containing three file system paths; the private SSH key file, 63 the public SSH key file and the user's C{authorized_keys} file 64 @raise errors.OpExecError: When home directory of the user can not be 65 determined 66 @raise errors.OpExecError: Regardless of the C{mkdir} parameters, this 67 exception is raised if C{~$user/.ssh} is not a directory and C{dircheck} 68 is set to C{True} 69 70 """ 71 if _homedir_fn is None: 72 _homedir_fn = utils.GetHomeDir 73 74 user_dir = _homedir_fn(user) 75 if not user_dir: 76 raise errors.OpExecError("Cannot resolve home of user '%s'" % user) 77 78 if kind == constants.SSHK_DSA: 79 suffix = "dsa" 80 elif kind == constants.SSHK_RSA: 81 suffix = "rsa" 82 else: 83 raise errors.ProgrammerError("Unknown SSH key kind '%s'" % kind) 84 85 ssh_dir = utils.PathJoin(user_dir, ".ssh") 86 if mkdir: 87 utils.EnsureDirs([(ssh_dir, constants.SECURE_DIR_MODE)]) 88 elif dircheck and not os.path.isdir(ssh_dir): 89 raise errors.OpExecError("Path %s is not a directory" % ssh_dir) 90 91 return [utils.PathJoin(ssh_dir, base) 92 for base in ["id_%s" % suffix, "id_%s.pub" % suffix, 93 "authorized_keys"]]
94 95
96 -def GetAllUserFiles(user, mkdir=False, dircheck=True, _homedir_fn=None):
97 """Wrapper over L{GetUserFiles} to retrieve files for all SSH key types. 98 99 See L{GetUserFiles} for details. 100 101 @rtype: tuple; (string, dict with string as key, tuple of (string, string) as 102 value) 103 104 """ 105 helper = compat.partial(GetUserFiles, user, mkdir=mkdir, dircheck=dircheck, 106 _homedir_fn=_homedir_fn) 107 result = [(kind, helper(kind=kind)) for kind in constants.SSHK_ALL] 108 109 authorized_keys = [i for (_, (_, _, i)) in result] 110 111 assert len(frozenset(authorized_keys)) == 1, \ 112 "Different paths for authorized_keys were returned" 113 114 return (authorized_keys[0], 115 dict((kind, (privkey, pubkey)) 116 for (kind, (privkey, pubkey, _)) in result))
117 118
119 -class SshRunner(object):
120 """Wrapper for SSH commands. 121 122 """
123 - def __init__(self, cluster_name):
124 """Initializes this class. 125 126 @type cluster_name: str 127 @param cluster_name: name of the cluster 128 129 """ 130 self.cluster_name = cluster_name 131 family = ssconf.SimpleStore().GetPrimaryIPFamily() 132 self.ipv6 = (family == netutils.IP6Address.family)
133
134 - def _BuildSshOptions(self, batch, ask_key, use_cluster_key, 135 strict_host_check, private_key=None, quiet=True, 136 port=None):
137 """Builds a list with needed SSH options. 138 139 @param batch: same as ssh's batch option 140 @param ask_key: allows ssh to ask for key confirmation; this 141 parameter conflicts with the batch one 142 @param use_cluster_key: if True, use the cluster name as the 143 HostKeyAlias name 144 @param strict_host_check: this makes the host key checking strict 145 @param private_key: use this private key instead of the default 146 @param quiet: whether to enable -q to ssh 147 @param port: the SSH port to use, or None to use the default 148 149 @rtype: list 150 @return: the list of options ready to use in L{utils.process.RunCmd} 151 152 """ 153 options = [ 154 "-oEscapeChar=none", 155 "-oHashKnownHosts=no", 156 "-oGlobalKnownHostsFile=%s" % pathutils.SSH_KNOWN_HOSTS_FILE, 157 "-oUserKnownHostsFile=/dev/null", 158 "-oCheckHostIp=no", 159 ] 160 161 if use_cluster_key: 162 options.append("-oHostKeyAlias=%s" % self.cluster_name) 163 164 if quiet: 165 options.append("-q") 166 167 if private_key: 168 options.append("-i%s" % private_key) 169 170 if port: 171 options.append("-oPort=%d" % port) 172 173 # TODO: Too many boolean options, maybe convert them to more descriptive 174 # constants. 175 176 # Note: ask_key conflicts with batch mode 177 if batch: 178 if ask_key: 179 raise errors.ProgrammerError("SSH call requested conflicting options") 180 181 options.append("-oBatchMode=yes") 182 183 if strict_host_check: 184 options.append("-oStrictHostKeyChecking=yes") 185 else: 186 options.append("-oStrictHostKeyChecking=no") 187 188 else: 189 # non-batch mode 190 191 if ask_key: 192 options.append("-oStrictHostKeyChecking=ask") 193 elif strict_host_check: 194 options.append("-oStrictHostKeyChecking=yes") 195 else: 196 options.append("-oStrictHostKeyChecking=no") 197 198 if self.ipv6: 199 options.append("-6") 200 else: 201 options.append("-4") 202 203 return options
204
205 - def BuildCmd(self, hostname, user, command, batch=True, ask_key=False, 206 tty=False, use_cluster_key=True, strict_host_check=True, 207 private_key=None, quiet=True, port=None):
208 """Build an ssh command to execute a command on a remote node. 209 210 @param hostname: the target host, string 211 @param user: user to auth as 212 @param command: the command 213 @param batch: if true, ssh will run in batch mode with no prompting 214 @param ask_key: if true, ssh will run with 215 StrictHostKeyChecking=ask, so that we can connect to an 216 unknown host (not valid in batch mode) 217 @param use_cluster_key: whether to expect and use the 218 cluster-global SSH key 219 @param strict_host_check: whether to check the host's SSH key at all 220 @param private_key: use this private key instead of the default 221 @param quiet: whether to enable -q to ssh 222 @param port: the SSH port on which the node's daemon is running 223 224 @return: the ssh call to run 'command' on the remote host. 225 226 """ 227 argv = [constants.SSH] 228 argv.extend(self._BuildSshOptions(batch, ask_key, use_cluster_key, 229 strict_host_check, private_key, 230 quiet=quiet, port=port)) 231 if tty: 232 argv.extend(["-t", "-t"]) 233 234 argv.append("%s@%s" % (user, hostname)) 235 236 # Insert variables for virtual nodes 237 argv.extend("export %s=%s;" % 238 (utils.ShellQuote(name), utils.ShellQuote(value)) 239 for (name, value) in 240 vcluster.EnvironmentForHost(hostname).items()) 241 242 argv.append(command) 243 244 return argv
245
246 - def Run(self, *args, **kwargs):
247 """Runs a command on a remote node. 248 249 This method has the same return value as `utils.RunCmd()`, which it 250 uses to launch ssh. 251 252 Args: see SshRunner.BuildCmd. 253 254 @rtype: L{utils.process.RunResult} 255 @return: the result as from L{utils.process.RunCmd()} 256 257 """ 258 return utils.RunCmd(self.BuildCmd(*args, **kwargs))
259
260 - def CopyFileToNode(self, node, port, filename):
261 """Copy a file to another node with scp. 262 263 @param node: node in the cluster 264 @param filename: absolute pathname of a local file 265 266 @rtype: boolean 267 @return: the success of the operation 268 269 """ 270 if not os.path.isabs(filename): 271 logging.error("File %s must be an absolute path", filename) 272 return False 273 274 if not os.path.isfile(filename): 275 logging.error("File %s does not exist", filename) 276 return False 277 278 command = [constants.SCP, "-p"] 279 command.extend(self._BuildSshOptions(True, False, True, True, port=port)) 280 command.append(filename) 281 if netutils.IP6Address.IsValid(node): 282 node = netutils.FormatAddress((node, None)) 283 284 command.append("%s:%s" % (node, vcluster.ExchangeNodeRoot(node, filename))) 285 286 result = utils.RunCmd(command) 287 288 if result.failed: 289 logging.error("Copy to node %s failed (%s) error '%s'," 290 " command was '%s'", 291 node, result.fail_reason, result.output, result.cmd) 292 293 return not result.failed
294
295 - def VerifyNodeHostname(self, node, ssh_port):
296 """Verify hostname consistency via SSH. 297 298 This functions connects via ssh to a node and compares the hostname 299 reported by the node to the name with have (the one that we 300 connected to). 301 302 This is used to detect problems in ssh known_hosts files 303 (conflicting known hosts) and inconsistencies between dns/hosts 304 entries and local machine names 305 306 @param node: nodename of a host to check; can be short or 307 full qualified hostname 308 @param ssh_port: the port of a SSH daemon running on the node 309 310 @return: (success, detail), where: 311 - success: True/False 312 - detail: string with details 313 314 """ 315 cmd = ("if test -z \"$GANETI_HOSTNAME\"; then" 316 " hostname --fqdn;" 317 "else" 318 " echo \"$GANETI_HOSTNAME\";" 319 "fi") 320 retval = self.Run(node, constants.SSH_LOGIN_USER, cmd, 321 quiet=False, port=ssh_port) 322 323 if retval.failed: 324 msg = "ssh problem" 325 output = retval.output 326 if output: 327 msg += ": %s" % output 328 else: 329 msg += ": %s (no output)" % retval.fail_reason 330 logging.error("Command %s failed: %s", retval.cmd, msg) 331 return False, msg 332 333 remotehostname = retval.stdout.strip() 334 335 if not remotehostname or remotehostname != node: 336 if node.startswith(remotehostname + "."): 337 msg = "hostname not FQDN" 338 else: 339 msg = "hostname mismatch" 340 return False, ("%s: expected %s but got %s" % 341 (msg, node, remotehostname)) 342 343 return True, "host matches"
344 345
346 -def WriteKnownHostsFile(cfg, file_name):
347 """Writes the cluster-wide equally known_hosts file. 348 349 """ 350 data = "" 351 if cfg.GetRsaHostKey(): 352 data += "%s ssh-rsa %s\n" % (cfg.GetClusterName(), cfg.GetRsaHostKey()) 353 if cfg.GetDsaHostKey(): 354 data += "%s ssh-dss %s\n" % (cfg.GetClusterName(), cfg.GetDsaHostKey()) 355 356 utils.WriteFile(file_name, mode=0600, data=data)
357