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