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