2 * Copyright (c) 2016 Red Hat, Inc. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
9 package org.opendaylight.ovsdb.utils.ovsdb.it.utils;
11 import java.io.BufferedReader;
13 import java.io.FileNotFoundException;
14 import java.io.FileReader;
15 import java.io.FileWriter;
16 import java.io.InputStreamReader;
17 import java.io.IOException;
18 import java.io.Reader;
19 import java.net.InetSocketAddress;
21 import java.nio.ByteBuffer;
22 import java.nio.channels.ClosedByInterruptException;
23 import java.nio.channels.SocketChannel;
24 import java.nio.charset.Charset;
25 import java.nio.charset.CharsetDecoder;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.List;
30 import java.util.concurrent.atomic.AtomicInteger;
32 import com.esotericsoftware.yamlbeans.YamlException;
33 import com.esotericsoftware.yamlbeans.YamlReader;
34 import org.junit.Assert;
35 import org.osgi.framework.Bundle;
36 import org.osgi.framework.FrameworkUtil;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
41 * Run OVS(s) using docker-compose for use in integration tests.
44 * try(DockerOvs ovs = new DockerOvs()) {
45 * ConnectionInfo connectionInfo = SouthboundUtils.getConnectionInfo(
46 * ovs.getOvsdbAddress(0), ovs.getOvsdbPort(0));
48 nodeInfo.disconnect();
50 * } catch (Exception e) {
54 * Nota bene, DockerOvs will check whether or not docker-compose command requires "sudo"
55 * to run. However, if it does require sudo, it must be configured to not prompt for a
56 * password ("NOPASSWD: ALL" is the sudoers file).
58 * DockerOvs loads its docker-compose yaml files from inside the ovsdb-it-utils bundle
59 * at the path META-INF/docker-compose-files/. Currently, a single yaml file is used,
60 * "docker-ovs-2.5.1.yml." DockerOvs does support docker-compose files that
61 * launch more than one docker image, more on this later. DockerOvs will wait for OVS
62 * to accept OVSDB connections. In order for this to work, the docker-compose file *must*
63 * have a port mapping.
64 * Currently, DockerOvs does not support docker images with OVS instances that connect actively.
66 public class DockerOvs implements AutoCloseable {
67 private static final Logger LOG = LoggerFactory.getLogger(DockerOvs.class);
68 public static final String DOCKER_SUDO = "docker.sudo";
69 private static final String DEFAULT_DOCKER_FILE = "docker-ovs-2.5.1.yml";
70 private static final String DOCKER_FILE_PATH = "META-INF/docker-compose-files/";
71 //private static final String[] HELP_CMD = {"docker-compose", "--help"};
72 //private static final String[] EXEC_CMD_PFX = {"sudo", "docker-compose", "-f"};
73 private static final int COMPOSE_FILE_IDX = 3;
74 private static final String DEFAULT_OVSDB_HOST = "127.0.0.1";
75 private static final String[] PS_CMD = {"sudo", "docker-compose", "ps"};
76 private static final String[] PS_CMD_NO_SUDO = {"docker-compose", "ps"};
78 private String[] upCmd = {"sudo", "docker-compose", "-f", null, "up", "-d"};
79 private String[] downCmd = {"sudo", "docker-compose", "-f", null, "stop"};
80 private File tmpDockerComposeFile;
81 private List<String> ovsdbPorts;
85 * Bring up all docker images in the default docker-compose file.
86 * @throws IOException if something goes wrong on the IO end
87 * @throws InterruptedException If this thread is interrupted
89 public DockerOvs() throws IOException, InterruptedException {
90 this(DEFAULT_DOCKER_FILE);
94 * Bring up all docker images in the provided docker-compose file under "META-INF/docker-compose-files/".
95 * @param yamlFileName Just the file name
96 * @throws IOException if something goes wrong on the IO end
97 * @throws InterruptedException If this thread is interrupted
99 public DockerOvs(String yamlFileName) throws IOException, InterruptedException {
100 tmpDockerComposeFile = createTempDockerComposeFile(yamlFileName);
101 buildDockerComposeCommands();
102 ovsdbPorts = extractPortsFromYaml();
105 //We run this for A LONG TIME since on the first run docker must download the
106 //image from docker hub. In experience it takes significantly less than this
107 //even when downloading the image. Once the image is downloaded this command
108 //runs like that <snaps fingers>
109 runProcess(60000, upCmd);
111 waitForOvsdbServers(10 * 1000);
115 * Verify and build the docker-compose commands we will be running. This function adds the docker-compose file
116 * to the command lines and also checks (and adjusts the command line) as to whether sudo is required. This is
117 * done by attempting to run "docker-compose ps" without and then with sudo
118 * @throws IOException if something goes wrong on the IO end
119 * @throws InterruptedException If this thread is interrupted
121 private void buildDockerComposeCommands() throws IOException, InterruptedException {
122 upCmd[COMPOSE_FILE_IDX] = tmpDockerComposeFile.toString();
123 downCmd[COMPOSE_FILE_IDX] = tmpDockerComposeFile.toString();
125 if (0 == tryProcess(5000, PS_CMD_NO_SUDO)) {
126 LOG.info("DockerOvs.buildDockerComposeCommands docker-compose does not require sudo");
128 tmp = Arrays.copyOfRange(upCmd, 1, upCmd.length);
130 tmp = Arrays.copyOfRange(downCmd, 1, downCmd.length);
132 } else if (0 == tryProcess(5000, PS_CMD)) {
133 LOG.info("DockerOvs.buildDockerComposeCommands docker-compose requires sudo");
135 Assert.fail("docker-compose does not seem to work with or without sudo");
139 * Get the IP address of the n'th OVS.
140 * @param ovsNumber which OVS?
143 public String getOvsdbAddress(int ovsNumber) {
144 return DEFAULT_OVSDB_HOST;
148 * Get the port of the n'th OVS.
149 * @param ovsNumber which OVS?
150 * @return Port as a string
152 public String getOvsdbPort(int ovsNumber) {
153 return ovsdbPorts.get(ovsNumber);
157 * How many OVS nodes are there.
158 * @return number of running OVS nodes
160 public int getNumOvsNodes() {
161 return ovsdbPorts.size();
165 * Parse the docker-compose yaml file to extract the port mappings.
166 * @return a list of the external ports
168 private List<String> extractPortsFromYaml() {
169 List<String> ports = new ArrayList<String>();
171 YamlReader yamlReader = null;
174 yamlReader = new YamlReader(new FileReader(tmpDockerComposeFile));
175 root = (Map) yamlReader.read();
176 } catch (FileNotFoundException e) {
177 LOG.warn("DockerOvs.extractPortsFromYaml error reading yaml file", e);
179 } catch (YamlException e) {
180 LOG.warn("DockerOvs.extractPortsFromYaml error parsing yaml file", e);
187 for (Object map : root.values()) {
188 List portMappings = (List) ((Map)map).get("ports");
189 if (null == portMappings) {
192 for (Object portMapping : portMappings) {
193 String portMappingStr = (String) portMapping;
194 int delim = portMappingStr.indexOf(":");
198 String port = portMappingStr.substring(0, delim);
207 * Shut everything down.
208 * @throws Exception but not really
211 public void close() throws Exception {
213 runProcess(5000, downCmd);
218 tmpDockerComposeFile.delete();
219 } catch (Exception ignored) {
220 //No reason to fail the test, we're just being polite here.
225 * A thread that waits until it can "ping" a running OVS - tests basic reachability
226 * and readiness. The "ping" here is actually a list_dbs method and the response is
227 * checked to make sure the Open_Vswitch DB is present. Note that this thread will
228 * run until it succeeds unless its interrupt() method is called.
230 class OvsdbPing extends Thread {
232 private final String host;
233 private final int port;
234 private final AtomicInteger result;
235 public CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
236 ByteBuffer listDbsRequest;
239 * Construct a new OvsdbPing object.
240 * @param ovsNumber which OVS is this?
241 * @param result an AtomicInteger that is incremented upon a successful "ping"
243 public OvsdbPing(int ovsNumber, AtomicInteger result) {
244 this.host = getOvsdbAddress(ovsNumber);
245 this.port = Integer.parseInt(getOvsdbPort(ovsNumber));
246 this.result = result;
247 listDbsRequest = ByteBuffer.wrap(
248 ("{\"method\": \"list_dbs\", \"params\": [], \"id\": " + port + "}").getBytes());
249 listDbsRequest.mark();
257 } catch (InterruptedException e) {
258 LOG.warn("OvsdbPing interrupted", e);
265 * Attempt a "ping" of the OVSDB connection.
266 * @return true if the ping was successful OR IF THIS THREAD WAS INTERRUPTED
268 private boolean doPing() {
269 try (SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(host, port))) {
270 socketChannel.write(listDbsRequest);
271 listDbsRequest.reset();
273 ByteBuffer buf = ByteBuffer.allocateDirect(512);
274 socketChannel.read(buf);
276 String response = decoder.decode(buf).toString();
278 if (response.contains("Open_vSwitch")) {
279 LOG.info("OvsdbPing connection validated");
280 result.incrementAndGet();
283 } catch (ClosedByInterruptException e) {
284 LOG.warn("OvsdbPing interrupted", e);
285 //return true here because we're done, ne'er to return again.
287 } catch (Exception e) {
288 LOG.info("OvsdbPing exception while attempting connect {}", e.toString());
295 * Wait for all Ovs's to accept and respond to OVSDB requests.
296 * @param waitFor How long to wait
297 * @throws IOException if something goes wrong on the IO end
298 * @throws InterruptedException If this thread is interrupted
300 private void waitForOvsdbServers(long waitFor) throws IOException, InterruptedException {
301 AtomicInteger numRunningOvs = new AtomicInteger(0);
303 int numOvs = ovsdbPorts.size();
308 OvsdbPing[] pingers = new OvsdbPing[numOvs];
309 for (int i = 0; i < numOvs; i++) {
310 pingers[i] = new OvsdbPing(i, numRunningOvs);
314 long startTime = System.currentTimeMillis();
315 while ( (System.currentTimeMillis() - startTime) < waitFor) {
316 if (numRunningOvs.get() >= numOvs) {
317 LOG.info("DockerOvs.waitForOvsdbServers all OVS instances running");
322 LOG.info("DockerOvs.waitForOvsdbServers - finished waiting in {}", System.currentTimeMillis() - startTime);
324 for (OvsdbPing pinger : pingers) {
330 WIP - todo: need to extract teh service name from the yaml or receive it as a param
331 private void validateDockerComposeVersion() throws IOException, InterruptedException {
332 StringBuilder stringBuilder = new StringBuilder();
333 runProcess(2000, stringBuilder, HELP_CMD);
334 assertTrue("DockerOvs.validateDockerComposeVersion: docker-compose version does not support exec, try updating",
335 stringBuilder.toString().contains(" exec "));
338 public String exec(long waitFor, String... execCmdWords) throws IOException, InterruptedException {
339 List<String> execCmd = new ArrayList<String>(20);
340 execCmd.addAll(Arrays.asList(EXEC_CMD_PFX));
341 execCmd.add(tmpDockerComposeFile.toString());
344 execCmd.addAll(Arrays.asList(execCmdWords));
346 StringBuilder stringBuilder = new StringBuilder();
347 runProcess(waitFor, stringBuilder, execCmd.toArray(new String[0]));
348 return stringBuilder.toString();
353 * Run a process and assert the exit code is 0.
354 * @param waitFor How long to wait for the command to execute
355 * @param words The words of the command to run
356 * @throws IOException if something goes wrong on the IO end
357 * @throws InterruptedException If this thread is interrupted
359 private void runProcess(long waitFor, String... words) throws IOException, InterruptedException {
360 runProcess(waitFor, null, words);
364 * Run a process, collect the stdout, and assert the exit code is 0.
365 * @param waitFor How long to wait for the command to execute
366 * @param capturedStdout Whatever the process wrote to standard out
367 * @param words The words of the command to run
368 * @throws IOException if something goes wrong on the IO end
369 * @throws InterruptedException If this thread is interrupted
371 private void runProcess(long waitFor,StringBuilder capturedStdout, String... words)
372 throws IOException, InterruptedException {
373 int exitValue = tryProcess(waitFor, capturedStdout, words);
374 Assert.assertEquals("DockerOvs.runProcess exit code is not 0", 0, exitValue);
379 * @param waitFor How long to wait for the command to execute
380 * @param words The words of the command to run
381 * @return The process's exit code
382 * @throws IOException if something goes wrong on the IO end
383 * @throws InterruptedException If this thread is interrupted
385 private int tryProcess(long waitFor, String... words) throws IOException, InterruptedException {
386 return tryProcess(waitFor, null, words);
390 * Run a process, collect the stdout.
391 * @param waitFor How long to wait (milliseconds) for the command to execute
392 * @param capturedStdout Whatever the process wrote to standard out
393 * @param words The words of the command to run
394 * @return The process's exit code or -1 if the the command does not complete within waitFor milliseconds
395 * @throws IOException if something goes wrong on the IO end
396 * @throws InterruptedException If this thread is interrupted
398 private int tryProcess(long waitFor, StringBuilder capturedStdout, String... words)
399 throws IOException, InterruptedException {
401 LOG.info("DockerOvs.runProcess running \"{}\", waitFor {}", words, waitFor);
403 Process proc = new ProcessBuilder(words).start();
406 // Use a try block to guarantee stdout and stderr are closed
407 try (BufferedReader stdout = new BufferedReader(new InputStreamReader(proc.getInputStream()));
408 BufferedReader stderr = new BufferedReader(new InputStreamReader(proc.getErrorStream()))) {
410 exitValue = waitForExitValue(waitFor, proc);
412 while (stderr.ready()) {
413 LOG.warn("DockerOvs.runProcess [stderr]: {}", stderr.readLine());
416 StringBuilder stdoutStringBuilder = (capturedStdout != null) ? capturedStdout : new StringBuilder();
418 char[] buf = new char[1024];
419 while (-1 != (read = stdout.read(buf))) {
420 stdoutStringBuilder.append(buf, 0, read);
423 for (String line : stdoutStringBuilder.toString().split("\\n")) {
424 LOG.info("DockerOvs.runProcess [stdout]: {}", line);
432 * Wait for a process to end.
433 * @param waitFor how long to wait in milliseconds
434 * @param proc Process object
435 * @return the process's exit value or -1 if the process did not complete within waitFor milliseconds
436 * @throws InterruptedException if this thread is interrupted
438 private int waitForExitValue(long waitFor, Process proc) throws InterruptedException {
439 //Java 7 has no way to check whether a process is still running without blocking
440 //until the process exits. What this hack does is checks the exitValue() which
441 //throws an IllegalStateException if the process is still running still it does
442 //not have a exit value. We catch that exception and implement our own timeout.
443 //Once we no longer need to support Java 7, this has more elegant solutions.
445 long startTime = System.currentTimeMillis();
448 exitValue = proc.exitValue();
450 } catch (IllegalThreadStateException e) {
451 if ((System.currentTimeMillis() - startTime) < waitFor) {
454 LOG.warn("DockerOvs.waitForExitValue: timed out while waiting for command to complete", e);
463 * Since the docker-compose file is a resource in the bundle and docker-compose needs it.
464 * in the file system, we copy it over - ugly but necessary.
465 * @param yamlFileName File name
466 * @return A File object for the newly created temporary yaml file.
468 private File createTempDockerComposeFile(String yamlFileName) {
469 Bundle bundle = FrameworkUtil.getBundle(this.getClass());
470 Assert.assertNotNull("DockerOvs: bundle is null", bundle);
471 URL url = bundle.getResource(DOCKER_FILE_PATH + yamlFileName);
472 Assert.assertNotNull("DockerOvs: URL is null", url);
476 tmpFile = File.createTempFile("ovsdb-it-tmp-", null);
478 try (Reader in = new InputStreamReader(url.openStream());
479 FileWriter out = new FileWriter(tmpFile)) {
480 char[] buf = new char[1024];
482 while (-1 != (read = in.read(buf))) {
483 out.write(buf, 0, read);
487 } catch (IOException e) {
488 Assert.fail(e.toString());