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