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