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