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