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 187 return options
188
189 - def BuildCmd(self, hostname, user, command, batch=True, ask_key=False, 190 tty=False, use_cluster_key=True, strict_host_check=True, 191 private_key=None, quiet=True):
192 """Build an ssh command to execute a command on a remote node. 193 194 @param hostname: the target host, string 195 @param user: user to auth as 196 @param command: the command 197 @param batch: if true, ssh will run in batch mode with no prompting 198 @param ask_key: if true, ssh will run with 199 StrictHostKeyChecking=ask, so that we can connect to an 200 unknown host (not valid in batch mode) 201 @param use_cluster_key: whether to expect and use the 202 cluster-global SSH key 203 @param strict_host_check: whether to check the host's SSH key at all 204 @param private_key: use this private key instead of the default 205 @param quiet: whether to enable -q to ssh 206 207 @return: the ssh call to run 'command' on the remote host. 208 209 """ 210 argv = [constants.SSH] 211 argv.extend(self._BuildSshOptions(batch, ask_key, use_cluster_key, 212 strict_host_check, private_key, 213 quiet=quiet)) 214 if tty: 215 argv.extend(["-t", "-t"]) 216 217 argv.append("%s@%s" % (user, hostname)) 218 219 # Insert variables for virtual nodes 220 argv.extend("export %s=%s;" % 221 (utils.ShellQuote(name), utils.ShellQuote(value)) 222 for (name, value) in 223 vcluster.EnvironmentForHost(hostname).items()) 224 225 argv.append(command) 226 227 return argv
228
229 - def Run(self, *args, **kwargs):
230 """Runs a command on a remote node. 231 232 This method has the same return value as `utils.RunCmd()`, which it 233 uses to launch ssh. 234 235 Args: see SshRunner.BuildCmd. 236 237 @rtype: L{utils.process.RunResult} 238 @return: the result as from L{utils.process.RunCmd()} 239 240 """ 241 return utils.RunCmd(self.BuildCmd(*args, **kwargs))
242
243 - def CopyFileToNode(self, node, filename):
244 """Copy a file to another node with scp. 245 246 @param node: node in the cluster 247 @param filename: absolute pathname of a local file 248 249 @rtype: boolean 250 @return: the success of the operation 251 252 """ 253 if not os.path.isabs(filename): 254 logging.error("File %s must be an absolute path", filename) 255 return False 256 257 if not os.path.isfile(filename): 258 logging.error("File %s does not exist", filename) 259 return False 260 261 command = [constants.SCP, "-p"] 262 command.extend(self._BuildSshOptions(True, False, True, True)) 263 command.append(filename) 264 if netutils.IP6Address.IsValid(node): 265 node = netutils.FormatAddress((node, None)) 266 267 command.append("%s:%s" % (node, vcluster.ExchangeNodeRoot(node, filename))) 268 269 result = utils.RunCmd(command) 270 271 if result.failed: 272 logging.error("Copy to node %s failed (%s) error '%s'," 273 " command was '%s'", 274 node, result.fail_reason, result.output, result.cmd) 275 276 return not result.failed
277
278 - def VerifyNodeHostname(self, node):
279 """Verify hostname consistency via SSH. 280 281 This functions connects via ssh to a node and compares the hostname 282 reported by the node to the name with have (the one that we 283 connected to). 284 285 This is used to detect problems in ssh known_hosts files 286 (conflicting known hosts) and inconsistencies between dns/hosts 287 entries and local machine names 288 289 @param node: nodename of a host to check; can be short or 290 full qualified hostname 291 292 @return: (success, detail), where: 293 - success: True/False 294 - detail: string with details 295 296 """ 297 cmd = ("if test -z \"$GANETI_HOSTNAME\"; then" 298 " hostname --fqdn;" 299 "else" 300 " echo \"$GANETI_HOSTNAME\";" 301 "fi") 302 retval = self.Run(node, constants.SSH_LOGIN_USER, cmd, quiet=False) 303 304 if retval.failed: 305 msg = "ssh problem" 306 output = retval.output 307 if output: 308 msg += ": %s" % output 309 else: 310 msg += ": %s (no output)" % retval.fail_reason 311 logging.error("Command %s failed: %s", retval.cmd, msg) 312 return False, msg 313 314 remotehostname = retval.stdout.strip() 315 316 if not remotehostname or remotehostname != node: 317 if node.startswith(remotehostname + "."): 318 msg = "hostname not FQDN" 319 else: 320 msg = "hostname mismatch" 321 return False, ("%s: expected %s but got %s" % 322 (msg, node, remotehostname)) 323 324 return True, "host matches"
325 326
327 -def WriteKnownHostsFile(cfg, file_name):
328 """Writes the cluster-wide equally known_hosts file. 329 330 """ 331 utils.WriteFile(file_name, mode=0600, 332 data="%s ssh-rsa %s\n" % (cfg.GetClusterName(), 333 cfg.GetHostKey()))
334