jaegonkim | dcf7c82 | 2019-02-06 15:00:14 +0900 | [diff] [blame^] | 1 | /* |
| 2 | * Copyright 2019-present Open Networking Foundation |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | package org.onosproject.ofoverlay.impl.util; |
| 17 | |
| 18 | import com.google.common.io.CharStreams; |
| 19 | import com.jcraft.jsch.Channel; |
| 20 | import com.jcraft.jsch.ChannelExec; |
| 21 | import com.jcraft.jsch.JSch; |
| 22 | import com.jcraft.jsch.JSchException; |
| 23 | import com.jcraft.jsch.Session; |
| 24 | import org.onlab.packet.IpAddress; |
| 25 | import org.onosproject.workflow.api.WorkflowException; |
| 26 | import org.onosproject.workflow.model.accessinfo.SshAccessInfo; |
| 27 | import org.slf4j.Logger; |
| 28 | |
| 29 | import java.io.BufferedReader; |
| 30 | import java.io.IOException; |
| 31 | import java.io.InputStream; |
| 32 | import java.io.InputStreamReader; |
| 33 | import java.nio.charset.StandardCharsets; |
| 34 | import java.util.Collections; |
| 35 | import java.util.Set; |
| 36 | import java.util.regex.Pattern; |
| 37 | import java.util.stream.Collectors; |
| 38 | |
| 39 | import static org.onosproject.workflow.api.CheckCondition.check; |
| 40 | import static org.slf4j.LoggerFactory.getLogger; |
| 41 | |
| 42 | /** |
| 43 | * Class for SSH utilities. |
| 44 | */ |
| 45 | public final class SshUtil { |
| 46 | |
| 47 | protected static final Logger log = getLogger(SshUtil.class); |
| 48 | |
| 49 | private static final String STRICT_HOST_CHECKING = "StrictHostKeyChecking"; |
| 50 | private static final String DEFAULT_STRICT_HOST_CHECKING = "no"; |
| 51 | private static final int DEFAULT_SESSION_TIMEOUT = 30000; // milliseconds |
| 52 | |
| 53 | private static final String SPACESEPERATOR = " "; |
| 54 | |
| 55 | /** |
| 56 | * Default constructor. |
| 57 | */ |
| 58 | private SshUtil() { |
| 59 | } |
| 60 | |
| 61 | /** |
| 62 | * Creates a new session with a given ssh access information. |
| 63 | * |
| 64 | * @param sshInfo information to ssh to the remote server |
| 65 | * @return ssh session, or null |
| 66 | */ |
| 67 | public static Session connect(SshAccessInfo sshInfo) { |
| 68 | Session session; |
| 69 | |
| 70 | try { |
| 71 | JSch jsch = new JSch(); |
| 72 | jsch.addIdentity(sshInfo.privateKey()); |
| 73 | |
| 74 | session = jsch.getSession(sshInfo.user(), |
| 75 | sshInfo.remoteIp().toString(), |
| 76 | sshInfo.port().toInt()); |
| 77 | session.setConfig(STRICT_HOST_CHECKING, DEFAULT_STRICT_HOST_CHECKING); |
| 78 | session.connect(DEFAULT_SESSION_TIMEOUT); |
| 79 | |
| 80 | } catch (JSchException e) { |
| 81 | log.warn("Failed to connect to {}", sshInfo.toString(), e); |
| 82 | session = authUserPwd(sshInfo); |
| 83 | } |
| 84 | return session; |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * Creates a new session with ssh access info. |
| 89 | * |
| 90 | * @param sshInfo information to ssh to the remote server |
| 91 | * @return ssh session, or null |
| 92 | */ |
| 93 | public static Session authUserPwd(SshAccessInfo sshInfo) { |
| 94 | log.info("Retrying Session with {}", sshInfo); |
| 95 | try { |
| 96 | JSch jsch = new JSch(); |
| 97 | |
| 98 | Session session = jsch.getSession(sshInfo.user(), |
| 99 | sshInfo.remoteIp().toString(), |
| 100 | sshInfo.port().toInt()); |
| 101 | session.setPassword(sshInfo.password()); |
| 102 | session.setConfig(STRICT_HOST_CHECKING, DEFAULT_STRICT_HOST_CHECKING); |
| 103 | session.connect(DEFAULT_SESSION_TIMEOUT); |
| 104 | |
| 105 | return session; |
| 106 | } catch (JSchException e) { |
| 107 | log.warn("Failed to connect to {} due to {}", sshInfo.toString(), e); |
| 108 | return null; |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | /** |
| 113 | * Closes a connection. |
| 114 | * |
| 115 | * @param session session ssh session |
| 116 | */ |
| 117 | public static void disconnect(Session session) { |
| 118 | if (session.isConnected()) { |
| 119 | session.disconnect(); |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | /** |
| 124 | * Fetches last term after executing command. |
| 125 | * @param session ssh session |
| 126 | * @param command command to execute |
| 127 | * @return last term, or null |
| 128 | */ |
| 129 | public static String fetchLastTerm(Session session, String command) { |
| 130 | if (session == null || !session.isConnected()) { |
| 131 | log.error("Invalid session({})", session); |
| 132 | return null; |
| 133 | } |
| 134 | |
| 135 | log.info("fetchLastTerm: ssh command {} to {}", command, session.getHost()); |
| 136 | |
| 137 | try { |
| 138 | Channel channel = session.openChannel("exec"); |
| 139 | if (channel == null) { |
| 140 | log.error("Invalid channel of session({}) for command({})", session, command); |
| 141 | return null; |
| 142 | } |
| 143 | |
| 144 | ((ChannelExec) channel).setCommand(command); |
| 145 | channel.setInputStream(null); |
| 146 | InputStream output = channel.getInputStream(); |
| 147 | channel.connect(); |
| 148 | String[] lineList = null; |
| 149 | |
| 150 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(output, StandardCharsets.UTF_8))) { |
| 151 | lineList = reader.lines().findFirst().get().split(SPACESEPERATOR); |
| 152 | } catch (IOException e) { |
| 153 | log.error("Exception in fetchLastTerm", e); |
| 154 | } finally { |
| 155 | channel.disconnect(); |
| 156 | output.close(); |
| 157 | } |
| 158 | |
| 159 | if (lineList.length > 0) { |
| 160 | return lineList[lineList.length - 1]; |
| 161 | } else { |
| 162 | return null; |
| 163 | } |
| 164 | |
| 165 | } catch (JSchException | IOException e) { |
| 166 | log.error("Exception in fetchLastTerm", e); |
| 167 | return null; |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | /** |
| 172 | * Executes a given command. It opens exec channel for the command and closes |
| 173 | * the channel when it's done. |
| 174 | * |
| 175 | * @param session ssh connection to a remote server |
| 176 | * @param command command to execute |
| 177 | * @return command output string if the command succeeds, or null |
| 178 | */ |
| 179 | public static String executeCommand(Session session, String command) { |
| 180 | if (session == null || !session.isConnected()) { |
| 181 | log.error("Invalid session({})", session); |
| 182 | return null; |
| 183 | } |
| 184 | |
| 185 | log.info("executeCommand: ssh command {} to {}", command, session.getHost()); |
| 186 | |
| 187 | try { |
| 188 | Channel channel = session.openChannel("exec"); |
| 189 | |
| 190 | if (channel == null) { |
| 191 | log.debug("Invalid channel of session({}) for command({})", session, command); |
| 192 | return null; |
| 193 | } |
| 194 | |
| 195 | ((ChannelExec) channel).setCommand(command); |
| 196 | channel.setInputStream(null); |
| 197 | InputStream output = channel.getInputStream(); |
| 198 | |
| 199 | channel.connect(); |
| 200 | String result = CharStreams.toString(new InputStreamReader(output, StandardCharsets.UTF_8)); |
| 201 | log.trace("SSH result(on {}): {}", session.getHost(), result); |
| 202 | channel.disconnect(); |
| 203 | |
| 204 | return result; |
| 205 | } catch (JSchException | IOException e) { |
| 206 | log.debug("Failed to execute command {} due to {}", command, e); |
| 207 | return null; |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | /** |
| 212 | * Fetches OVS version information. |
| 213 | * @param session Jsch session |
| 214 | * @return OVS version |
| 215 | * @throws WorkflowException workflow exception |
| 216 | */ |
| 217 | public static OvsVersion fetchOvsVersion(Session session) throws WorkflowException { |
| 218 | |
| 219 | OvsVersion devOvsVersion; |
| 220 | |
| 221 | String ovsVersionStr = fetchLastTerm(session, "ovs-vswitchd --version"); |
| 222 | if (ovsVersionStr == null) { |
| 223 | log.error("Failed to get ovs Version String for ssh session:{}", session); |
| 224 | throw new WorkflowException("Failed to get ovs Version String"); |
| 225 | } |
| 226 | |
| 227 | devOvsVersion = OvsVersion.build(ovsVersionStr); |
| 228 | if (devOvsVersion == null) { |
| 229 | log.error("Failed to build OVS version for {}", ovsVersionStr); |
| 230 | throw new WorkflowException("Failed to build OVS version"); |
| 231 | } |
| 232 | |
| 233 | return devOvsVersion; |
| 234 | } |
| 235 | |
| 236 | private static final String IP_PATTERN = "^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + |
| 237 | "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + |
| 238 | "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + |
| 239 | "([01]?\\d\\d?|2[0-4]\\d|25[0-5])$"; |
| 240 | |
| 241 | private static boolean isIPv6(String address) { |
| 242 | boolean isCorrect = true; |
| 243 | try { |
| 244 | IpAddress.valueOf(address); |
| 245 | } catch (IllegalArgumentException e) { |
| 246 | log.debug("Exception Occurred {}", e.toString()); |
| 247 | isCorrect = false; |
| 248 | } |
| 249 | return isCorrect; |
| 250 | } |
| 251 | |
| 252 | private static boolean isCidr(String s) { |
| 253 | String[] splits = s.split("/"); |
| 254 | return splits.length == 2 && |
| 255 | (splits[0].matches(IP_PATTERN) || isIPv6(splits[0])); |
| 256 | } |
| 257 | |
| 258 | /** |
| 259 | * Adds IP address on the interface. |
| 260 | * @param session SSH session |
| 261 | * @param ifname interface name |
| 262 | * @param address network address |
| 263 | * @throws WorkflowException workflow exception |
| 264 | */ |
| 265 | public static void addIpAddrOnInterface(Session session, String ifname, NetworkAddress address) |
| 266 | throws WorkflowException { |
| 267 | |
| 268 | executeCommand(session, String.format("ip addr add %s dev %s", address.cidr(), ifname)); |
| 269 | |
| 270 | Set<NetworkAddress> result = getIpAddrOfInterface(session, ifname); |
| 271 | if (!result.contains(address)) { |
| 272 | throw new WorkflowException("Failed to set ip(" + address + ") on " + ifname + ", result: " + result); |
| 273 | } |
| 274 | } |
| 275 | |
| 276 | /** |
| 277 | * Gets IP addresses of interface. |
| 278 | * @param session SSH session |
| 279 | * @param ifname interface name |
| 280 | * @return IP addresses of interface |
| 281 | */ |
| 282 | public static Set<NetworkAddress> getIpAddrOfInterface(Session session, String ifname) { |
| 283 | |
| 284 | String output = executeCommand(session, String.format("ip addr show %s", ifname)); |
| 285 | |
| 286 | if (output == null) { |
| 287 | return Collections.emptySet(); |
| 288 | } |
| 289 | |
| 290 | Set<NetworkAddress> result = Pattern.compile(" ") |
| 291 | .splitAsStream(output) |
| 292 | .filter(SshUtil::isCidr) |
| 293 | .map(NetworkAddress::valueOf) |
| 294 | .collect(Collectors.toSet()); |
| 295 | return result; |
| 296 | } |
| 297 | |
| 298 | /** |
| 299 | * Returns whether the interface has IP address. |
| 300 | * @param session SSH session |
| 301 | * @param ifname interface name |
| 302 | * @param addr network address |
| 303 | * @return whether the interface has IP address |
| 304 | */ |
| 305 | public static boolean hasIpAddrOnInterface(Session session, String ifname, NetworkAddress addr) { |
| 306 | |
| 307 | Set<NetworkAddress> phyBrIps = getIpAddrOfInterface(session, ifname); |
| 308 | |
| 309 | return phyBrIps.stream() |
| 310 | .anyMatch(ip -> addr.ip().equals(ip.ip())); |
| 311 | } |
| 312 | |
| 313 | /** |
| 314 | * Sets IP link UP on the interface. |
| 315 | * @param session SSH session |
| 316 | * @param ifname interface name |
| 317 | * @throws WorkflowException workflow exception |
| 318 | */ |
| 319 | public static void setIpLinkUpOnInterface(Session session, String ifname) |
| 320 | throws WorkflowException { |
| 321 | |
| 322 | executeCommand(session, String.format("ip link set %s up", ifname)); |
| 323 | |
| 324 | if (!isIpLinkUpOnInterface(session, ifname)) { |
| 325 | throw new WorkflowException("Failed to set UP on " + ifname); |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | /** |
| 330 | * Returns whether the link of the interface is up. |
| 331 | * @param session SSH session |
| 332 | * @param ifname interface name |
| 333 | * @return whether the link of the interface is up |
| 334 | */ |
| 335 | public static boolean isIpLinkUpOnInterface(Session session, String ifname) { |
| 336 | String output = executeCommand(session, String.format("ip link show %s", ifname)); |
| 337 | |
| 338 | return output != null && output.contains("UP"); |
| 339 | } |
| 340 | |
| 341 | /** |
| 342 | * Executes SSH behavior. |
| 343 | * @param sshAccessInfo SSH Access information |
| 344 | * @param behavior SSH behavior |
| 345 | * @param <R> Return type of SSH behavior |
| 346 | * @return return of SSH behavior |
| 347 | * @throws WorkflowException workflow exception |
| 348 | */ |
| 349 | public static <R> R exec(SshAccessInfo sshAccessInfo, SshBehavior<R> behavior) |
| 350 | throws WorkflowException { |
| 351 | |
| 352 | check(sshAccessInfo != null, "Invalid sshAccessInfo"); |
| 353 | Session session = connect(sshAccessInfo); |
| 354 | if (session == null || !session.isConnected()) { |
| 355 | log.error("Failed to get session for ssh:{}", sshAccessInfo); |
| 356 | throw new WorkflowException("Failed to get session for ssh:" + sshAccessInfo); |
| 357 | } |
| 358 | |
| 359 | try { |
| 360 | return behavior.apply(session); |
| 361 | } finally { |
| 362 | disconnect(session); |
| 363 | } |
| 364 | } |
| 365 | |
| 366 | } |