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 elif kind == constants.SSHK_ECDSA: 83 suffix = "ecdsa" 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 -def GetAllUserFiles(user, mkdir=False, dircheck=True, _homedir_fn=None):
99 """Wrapper over L{GetUserFiles} to retrieve files for all SSH key types. 100 101 See L{GetUserFiles} for details. 102 103 @rtype: tuple; (string, dict with string as key, tuple of (string, string) as 104 value) 105 106 """ 107 helper = compat.partial(GetUserFiles, user, mkdir=mkdir, dircheck=dircheck, 108 _homedir_fn=_homedir_fn) 109 result = [(kind, helper(kind=kind)) for kind in constants.SSHK_ALL] 110 111 authorized_keys = [i for (_, (_, _, i)) in result] 112 113 assert len(frozenset(authorized_keys)) == 1, \ 114 "Different paths for authorized_keys were returned" 115 116 return (authorized_keys[0], 117 dict((kind, (privkey, pubkey)) 118 for (kind, (privkey, pubkey, _)) in result))
119 120
121 -class SshRunner(object):
122 """Wrapper for SSH commands. 123 124 """
125 - def __init__(self, cluster_name):
126 """Initializes this class. 127 128 @type cluster_name: str 129 @param cluster_name: name of the cluster 130 131 """ 132 self.cluster_name = cluster_name 133 family = ssconf.SimpleStore().GetPrimaryIPFamily() 134 self.ipv6 = (family == netutils.IP6Address.family)
135
136 - def _BuildSshOptions(self, batch, ask_key, use_cluster_key, 137 strict_host_check, private_key=None, quiet=True, 138 port=None):
139 """Builds a list with needed SSH options. 140 141 @param batch: same as ssh's batch option 142 @param ask_key: allows ssh to ask for key confirmation; this 143 parameter conflicts with the batch one 144 @param use_cluster_key: if True, use the cluster name as the 145 HostKeyAlias name 146 @param strict_host_check: this makes the host key checking strict 147 @param private_key: use this private key instead of the default 148 @param quiet: whether to enable -q to ssh 149 @param port: the SSH port to use, or None to use the default 150 151 @rtype: list 152 @return: the list of options ready to use in L{utils.process.RunCmd} 153 154 """ 155 options = [ 156 "-oEscapeChar=none", 157 "-oHashKnownHosts=no", 158 "-oGlobalKnownHostsFile=%s" % pathutils.SSH_KNOWN_HOSTS_FILE, 159 "-oUserKnownHostsFile=/dev/null", 160 "-oCheckHostIp=no", 161 ] 162 163 if use_cluster_key: 164 options.append("-oHostKeyAlias=%s" % self.cluster_name) 165 166 if quiet: 167 options.append("-q") 168 169 if private_key: 170 options.append("-i%s" % private_key) 171 172 if port: 173 options.append("-oPort=%d" % port) 174 175 # TODO: Too many boolean options, maybe convert them to more descriptive 176 # constants. 177 178 # Note: ask_key conflicts with batch mode 179 if batch: 180 if ask_key: 181 raise errors.ProgrammerError("SSH call requested conflicting options") 182 183 options.append("-oBatchMode=yes") 184 185 if strict_host_check: 186 options.append("-oStrictHostKeyChecking=yes") 187 else: 188 options.append("-oStrictHostKeyChecking=no") 189 190 else: 191 # non-batch mode 192 193 if ask_key: 194 options.append("-oStrictHostKeyChecking=ask") 195 elif strict_host_check: 196 options.append("-oStrictHostKeyChecking=yes") 197 else: 198 options.append("-oStrictHostKeyChecking=no") 199 200 if self.ipv6: 201 options.append("-6") 202 else: 203 options.append("-4") 204 205 return options
206
207 - def BuildCmd(self, hostname, user, command, batch=True, ask_key=False, 208 tty=False, use_cluster_key=True, strict_host_check=True, 209 private_key=None, quiet=True, port=None):
210 """Build an ssh command to execute a command on a remote node. 211 212 @param hostname: the target host, string 213 @param user: user to auth as 214 @param command: the command 215 @param batch: if true, ssh will run in batch mode with no prompting 216 @param ask_key: if true, ssh will run with 217 StrictHostKeyChecking=ask, so that we can connect to an 218 unknown host (not valid in batch mode) 219 @param use_cluster_key: whether to expect and use the 220 cluster-global SSH key 221 @param strict_host_check: whether to check the host's SSH key at all 222 @param private_key: use this private key instead of the default 223 @param quiet: whether to enable -q to ssh 224 @param port: the SSH port on which the node's daemon is running 225 226 @return: the ssh call to run 'command' on the remote host. 227 228 """ 229 argv = [constants.SSH] 230 argv.extend(self._BuildSshOptions(batch, ask_key, use_cluster_key, 231 strict_host_check, private_key, 232 quiet=quiet, port=port)) 233 if tty: 234 argv.extend(["-t", "-t"]) 235 236 argv.append("%s@%s" % (user, hostname)) 237 238 # Insert variables for virtual nodes 239 argv.extend("export %s=%s;" % 240 (utils.ShellQuote(name), utils.ShellQuote(value)) 241 for (name, value) in 242 vcluster.EnvironmentForHost(hostname).items()) 243 244 argv.append(command) 245 246 return argv
247
248 - def Run(self, *args, **kwargs):
249 """Runs a command on a remote node. 250 251 This method has the same return value as `utils.RunCmd()`, which it 252 uses to launch ssh. 253 254 Args: see SshRunner.BuildCmd. 255 256 @rtype: L{utils.process.RunResult} 257 @return: the result as from L{utils.process.RunCmd()} 258 259 """ 260 return utils.RunCmd(self.BuildCmd(*args, **kwargs))
261
262 - def CopyFileToNode(self, node, port, filename):
263 """Copy a file to another node with scp. 264 265 @param node: node in the cluster 266 @param filename: absolute pathname of a local file 267 268 @rtype: boolean 269 @return: the success of the operation 270 271 """ 272 if not os.path.isabs(filename): 273 logging.error("File %s must be an absolute path", filename) 274 return False 275 276 if not os.path.isfile(filename): 277 logging.error("File %s does not exist", filename) 278 return False 279 280 command = [constants.SCP, "-p"] 281 command.extend(self._BuildSshOptions(True, False, True, True, port=port)) 282 command.append(filename) 283 if netutils.IP6Address.IsValid(node): 284 node = netutils.FormatAddress((node, None)) 285 286 command.append("%s:%s" % (node, vcluster.ExchangeNodeRoot(node, filename))) 287 288 result = utils.RunCmd(command) 289 290 if result.failed: 291 logging.error("Copy to node %s failed (%s) error '%s'," 292 " command was '%s'", 293 node, result.fail_reason, result.output, result.cmd) 294 295 return not result.failed
296
297 - def VerifyNodeHostname(self, node, ssh_port):
298 """Verify hostname consistency via SSH. 299 300 This functions connects via ssh to a node and compares the hostname 301 reported by the node to the name with have (the one that we 302 connected to). 303 304 This is used to detect problems in ssh known_hosts files 305 (conflicting known hosts) and inconsistencies between dns/hosts 306 entries and local machine names 307 308 @param node: nodename of a host to check; can be short or 309 full qualified hostname 310 @param ssh_port: the port of a SSH daemon running on the node 311 312 @return: (success, detail), where: 313 - success: True/False 314 - detail: string with details 315 316 """ 317 cmd = ("if test -z \"$GANETI_HOSTNAME\"; then" 318 " hostname --fqdn;" 319 "else" 320 " echo \"$GANETI_HOSTNAME\";" 321 "fi") 322 retval = self.Run(node, constants.SSH_LOGIN_USER, cmd, 323 quiet=False, port=ssh_port) 324 325 if retval.failed: 326 msg = "ssh problem" 327 output = retval.output 328 if output: 329 msg += ": %s" % output 330 else: 331 msg += ": %s (no output)" % retval.fail_reason 332 logging.error("Command %s failed: %s", retval.cmd, msg) 333 return False, msg 334 335 remotehostname = retval.stdout.strip() 336 337 if not remotehostname or remotehostname != node: 338 if node.startswith(remotehostname + "."): 339 msg = "hostname not FQDN" 340 else: 341 msg = "hostname mismatch" 342 return False, ("%s: expected %s but got %s" % 343 (msg, node, remotehostname)) 344 345 return True, "host matches"
346 347
348 -def WriteKnownHostsFile(cfg, file_name):
349 """Writes the cluster-wide equally known_hosts file. 350 351 """ 352 data = "" 353 if cfg.GetRsaHostKey(): 354 data += "%s ssh-rsa %s\n" % (cfg.GetClusterName(), cfg.GetRsaHostKey()) 355 if cfg.GetDsaHostKey(): 356 data += "%s ssh-dss %s\n" % (cfg.GetClusterName(), cfg.GetDsaHostKey()) 357 358 utils.WriteFile(file_name, mode=0600, data=data)
359