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