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