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