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;
12 import java.io.FileNotFoundException;
13 import java.io.FileReader;
14 import java.io.FileWriter;
15 import java.io.InputStreamReader;
16 import java.io.IOException;
17 import java.io.Reader;
18 import java.net.InetSocketAddress;
20 import java.nio.ByteBuffer;
21 import java.nio.channels.ClosedByInterruptException;
22 import java.nio.channels.SocketChannel;
23 import java.nio.charset.Charset;
24 import java.nio.charset.CharsetDecoder;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.List;
29 import java.util.Properties;
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.ops4j.pax.exam.Option;
36 import org.osgi.framework.Bundle;
37 import org.osgi.framework.FrameworkUtil;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
41 import static org.ops4j.pax.exam.CoreOptions.propagateSystemProperties;
44 * Run OVS(s) using docker-compose for use in integration tests.
47 * try(DockerOvs ovs = new DockerOvs()) {
48 * ConnectionInfo connectionInfo = SouthboundUtils.getConnectionInfo(
49 * ovs.getOvsdbAddress(0), ovs.getOvsdbPort(0));
51 * nodeInfo.disconnect();
53 * } catch (Exception e) {
57 * Nota bene, DockerOvs will check whether or not docker-compose command requires "sudo"
58 * to run. However, if it does require sudo, it must be configured to not prompt for a
59 * password ("NOPASSWD: ALL" is the sudoers file).
61 * DockerOvs loads its docker-compose yaml files from inside the ovsdb-it-utils bundle
62 * at the path META-INF/docker-compose-files/. Currently, a single yaml file is used,
63 * "docker-ovs-2.5.1.yml." DockerOvs does support docker-compose files that
64 * launch more than one docker image, more on this later. DockerOvs will wait for OVS
65 * to accept OVSDB connections.
66 * Any docker-compose file must have a port mapping.
68 * The following explains how system properties are used to configure DockerOvs
70 * private static String ENV_USAGE =
71 * "-Ddocker.run - explicitly configure whether or not DockerOvs should run docker-compose\n" +
72 * "-Dovsdbserver.ipaddress - specify IP address of ovsdb server - implies -Ddocker.run=false\n" +
73 * "-Dovsdbserver.port - specify the port of the ovsdb server - required with -Dovsdbserver.ipaddress\n" +
74 * "-Ddocker.compose.file - docker compose file in META-INF/docker-compose-files/. If not specified, default file is used\n" +
75 * "-Dovsdb.userspace.enabled - true when Ovs is running in user space (usually the case with docker)\n" +
76 * "-Dovsdb.controller.address - IP address of the controller (usually the docker0 interface with docker)\n" +
77 * "To auto-run Ovs and connect actively:\n" +
78 * " -Dovsdb.controller.address=x.x.x.x -Dovsdb.userspace.enabled=yes [-Ddocker.compose.file=ffff]\n" +
79 * "To auto-run Ovs and connect passively:\n" +
80 * " -Dovsdbserver.connection=passive -Dovsdb.controller.address=x.x.x.x -Dovsdb.userspace.enabled=yes [-Ddocker.compose.file=ffff]\n" +
81 * "To actively connect to a running Ovs:\n" +
82 * " -Dovsdbserver.ipaddress=x.x.x.x -Dovsdbserver.port=6641 -Dovsdb.controller.address=y.y.y.y\n" +
83 * "To passively connect to a running Ovs:\n" +
84 * " -Dovsdbserver.connection=passive -Ddocker.run=false\n";
86 * When DockerOvs does not run docker-compose getOvsdbAddress and getOvsdbPort return the address and port specified in
87 * the system properties.
89 public class DockerOvs implements AutoCloseable {
90 private static String ENV_USAGE = "Usage:\n" +
91 "-Ddocker.run - explicitly configure whether or not DockerOvs should run docker-compose\n" +
92 "-Dovsdbserver.ipaddress - specify IP address of ovsdb server - implies -Ddocker.run=false\n" +
93 "-Dovsdbserver.port - specify the port of the ovsdb server - required with -Dovsdbserver.ipaddress\n" +
94 "-Ddocker.compose.file - docker compose file in META-INF/docker-compose-files/. If not specified, default file is used\n" +
95 "-Dovsdb.userspace.enabled - true when Ovs is running in user space (usually the case with docker)\n" +
96 "-Dovsdb.controller.address - IP address of the controller (usually the docker0 interface with docker)\n" +
97 "To auto-run Ovs and connect actively:\n" +
98 " -Dovsdb.controller.address=x.x.x.x -Dovsdb.userspace.enabled=yes <-Ddocker.compose.file=ffff>\n" +
99 "To auto-run Ovs and connect passively:\n" +
100 " -Dovsdbserver.connection=passive -Dovsdb.controller.address=x.x.x.x -Dovsdb.userspace.enabled=yes <-Ddocker.compose.file=ffff>\n" +
101 "To actively connect to a running Ovs:\n" +
102 " -Dovsdbserver.ipaddress=x.x.x.x -Dovsdbserver.port=6641 -Dovsdb.controller.address=y.y.y.y\n" +
103 "To passively connect to a running Ovs:\n" +
104 " -Dovsdbserver.connection=passive -Ddocker.run=false\n";
106 private static final Logger LOG = LoggerFactory.getLogger(DockerOvs.class);
107 private static final String DEFAULT_DOCKER_FILE = "docker-ovs-2.5.1.yml";
108 private static final String DOCKER_FILE_PATH = "META-INF/docker-compose-files/";
109 private static final int COMPOSE_FILE_IDX = 3;
110 private static final int COMPOSE_FILE_IDX_NO_SUDO = 2;
111 private static final String DEFAULT_OVSDB_HOST = "127.0.0.1";
113 private String[] psCmd = {"sudo", "docker-compose", "-f", null, "ps"};
114 private String[] psCmdNoSudo = {"docker-compose", "-f", null, "ps"};
115 private String[] upCmd = {"sudo", "docker-compose", "-f", null, "up", "-d", "--force-recreate"};
116 private String[] downCmd = {"sudo", "docker-compose", "-f", null, "stop"};
117 private String[] execCmd = {"sudo", "docker-compose", "-f", null, "exec", null};
119 private File tmpDockerComposeFile;
121 private String envServerAddress;
122 private String envServerPort;
123 private String envDockerComposeFile;
124 private boolean runDocker;
126 class DockerComposeServiceInfo {
130 private List<DockerComposeServiceInfo> dockerComposeServices = new ArrayList<DockerComposeServiceInfo>();
133 * Get the array of system properties as pax exam Option objects for use in pax exam
134 * unit tests with Configuration annotation.
135 * @return List of Option objects
137 public static Option[] getSysPropOptions() {
138 return new Option[] {
139 propagateSystemProperties(ItConstants.SERVER_IPADDRESS,
140 ItConstants.SERVER_PORT,
141 ItConstants.CONNECTION_TYPE,
142 ItConstants.CONTROLLER_IPADDRESS,
143 ItConstants.USERSPACE_ENABLED,
144 ItConstants.DOCKER_COMPOSE_FILE_NAME,
145 ItConstants.DOCKER_RUN)
150 * Bring up all docker images in the default docker-compose file.
151 * @throws IOException if something goes wrong on the IO end
152 * @throws InterruptedException If this thread is interrupted
154 public DockerOvs() throws IOException, InterruptedException {
155 this(DEFAULT_DOCKER_FILE);
159 * Bring up all docker images in the provided docker-compose file under "META-INF/docker-compose-files/".
160 * @param yamlFileName Just the file name
161 * @throws IOException if something goes wrong on the IO end
162 * @throws InterruptedException If this thread is interrupted
164 public DockerOvs(String yamlFileName) throws IOException, InterruptedException {
168 LOG.info("DockerOvs.DockerOvs: Not running docker, -D{} specified", ItConstants.SERVER_IPADDRESS);
172 tmpDockerComposeFile = createTempDockerComposeFile(yamlFileName);
173 buildDockerComposeCommands();
174 parseDockerComposeYaml();
177 //We run this for A LONG TIME since on the first run docker must download the
178 //image from docker hub. In experience it takes significantly less than this
179 //even when downloading the image. Once the image is downloaded this command
180 //runs like that <snaps fingers>
181 ProcUtils.runProcess(60000, upCmd);
183 waitForOvsdbServers(10 * 1000);
187 * Pull required configuration from System.getProperties() and validate we have what we need.
188 * Note: Note that there is some minor complexity in how this class is configured using System
189 * properties. This stems from the fact that we want to preserve the meaning of these properties
190 * prior to the introduction of this class. See the ENV_USAGE variable for details.
192 private void configureFromEnv() {
193 Properties env = System.getProperties();
194 envServerAddress = env.getProperty(ItConstants.SERVER_IPADDRESS);
195 envServerPort = env.getProperty(ItConstants.SERVER_PORT);
196 String envRunDocker = env.getProperty(ItConstants.DOCKER_RUN);
197 String connType = env.getProperty(ItConstants.CONNECTION_TYPE, ItConstants.CONNECTION_TYPE_ACTIVE);
198 String dockerFile = env.getProperty(ItConstants.DOCKER_COMPOSE_FILE_NAME);
199 envDockerComposeFile = DOCKER_FILE_PATH + (null == dockerFile ? DEFAULT_DOCKER_FILE : dockerFile);
201 //Are we running docker? If we specified docker.run, that's the answer. Otherwise, if there is a server
202 //address we assume docker is already running
203 runDocker = (envRunDocker != null) ? Boolean.parseBoolean(envRunDocker) : envServerAddress == null;
209 if (connType.equals(ItConstants.CONNECTION_TYPE_PASSIVE)) {
213 //At this point we know we're not running docker and the conn type is active - make sure we have what we need
214 //If we have a server address than we require a port too as those
215 //are returned in getOvsdbPort() and getOvsdbAddress()
216 Assert.assertNotNull("Attempt to connect to previous running ovs but missing -Dovsdbserver.ipaddress\n"
217 + ENV_USAGE, envServerAddress);
218 Assert.assertNotNull("Attempt to connect to previous running ovs but missing -Dovsdbserver.port\n"
219 + ENV_USAGE, envServerPort);
223 * Verify and build the docker-compose commands we will be running. This function adds the docker-compose file
224 * to the command lines and also checks (and adjusts the command line) as to whether sudo is required. This is
225 * done by attempting to run "docker-compose ps" without and then with sudo
226 * @throws IOException if something goes wrong on the IO end
227 * @throws InterruptedException If this thread is interrupted
229 private void buildDockerComposeCommands() throws IOException, InterruptedException {
230 psCmd[COMPOSE_FILE_IDX] = tmpDockerComposeFile.toString();
231 psCmdNoSudo[COMPOSE_FILE_IDX_NO_SUDO] = tmpDockerComposeFile.toString();
232 upCmd[COMPOSE_FILE_IDX] = tmpDockerComposeFile.toString();
233 downCmd[COMPOSE_FILE_IDX] = tmpDockerComposeFile.toString();
234 execCmd[COMPOSE_FILE_IDX] = tmpDockerComposeFile.toString();
236 if (0 == ProcUtils.tryProcess(5000, psCmdNoSudo)) {
237 LOG.info("DockerOvs.buildDockerComposeCommands docker-compose does not require sudo");
239 tmp = Arrays.copyOfRange(upCmd, 1, upCmd.length);
241 tmp = Arrays.copyOfRange(downCmd, 1, downCmd.length);
243 tmp = Arrays.copyOfRange(execCmd, 1, execCmd.length);
245 } else if (0 == ProcUtils.tryProcess(5000, psCmd)) {
246 LOG.info("DockerOvs.buildDockerComposeCommands docker-compose requires sudo");
248 Assert.fail("docker-compose does not seem to work with or without sudo");
253 * Are we using some other OVS, not a docker we spin up?
254 * @return true if we are *not* running a docker image to test against
256 public boolean usingExternalDocker() {
261 * Get the IP address of the n'th OVS.
262 * @param ovsNumber which OVS?
265 public String getOvsdbAddress(int ovsNumber) {
267 return envServerAddress;
269 return DEFAULT_OVSDB_HOST;
273 * Get the port of the n'th OVS.
274 * @param ovsNumber which OVS?
275 * @return Port as a string
277 public String getOvsdbPort(int ovsNumber) {
279 return envServerPort;
281 return dockerComposeServices.get(ovsNumber).port;
285 * How many OVS nodes are there.
286 * @return number of running OVS nodes
288 public int getNumOvsNodes() {
289 return dockerComposeServices.size();
292 public String[] getExecCmdPrefix(int numOvs) {
293 String[] res = new String[execCmd.length];
294 System.arraycopy(execCmd, 0, res, 0, execCmd.length);
295 res[res.length - 1] = dockerComposeServices.get(numOvs).name;
299 public void runInContainer(int waitFor, int numOvs, String ... cmdWords) throws IOException, InterruptedException {
300 String[] pfx = getExecCmdPrefix(numOvs);
301 String[] cmd = new String[pfx.length + cmdWords.length];
302 System.arraycopy(pfx, 0, cmd, 0, pfx.length);
303 System.arraycopy(cmdWords, 0, cmd, pfx.length, cmdWords.length);
304 ProcUtils.runProcess(waitFor, cmd);
307 public void tryInContainer(int waitFor, int numOvs, String ... cmdWords) throws IOException, InterruptedException {
308 String[] pfx = getExecCmdPrefix(numOvs);
309 String[] cmd = new String[pfx.length + cmdWords.length];
310 System.arraycopy(pfx, 0, cmd, 0, pfx.length);
311 System.arraycopy(cmdWords, 0, cmd, pfx.length, cmdWords.length);
312 ProcUtils.tryProcess(waitFor, cmd);
316 * Parse the docker-compose yaml file to extract the port mappings.
317 * @return a list of the external ports
319 private List<String> parseDockerComposeYaml() {
320 List<String> ports = new ArrayList<String>();
322 YamlReader yamlReader = null;
325 yamlReader = new YamlReader(new FileReader(tmpDockerComposeFile));
326 root = (Map) yamlReader.read();
327 } catch (FileNotFoundException e) {
328 LOG.warn("DockerOvs.parseDockerComposeYaml error reading yaml file", e);
330 } catch (YamlException e) {
331 LOG.warn("DockerOvs.parseDockerComposeYaml error parsing yaml file", e);
338 for (Object entry : root.entrySet()) {
339 String key = ((Map.Entry<String,Map>)entry).getKey();
340 Map map = ((Map.Entry<String,Map>)entry).getValue();
342 DockerComposeServiceInfo svc = new DockerComposeServiceInfo();
345 List portMappings = (List) map.get("ports");
346 if (null == portMappings) {
349 for (Object portMapping : portMappings) {
350 String portMappingStr = (String) portMapping;
351 int delim = portMappingStr.indexOf(":");
355 String port = portMappingStr.substring(0, delim);
359 //TODO: think this through. What if there is no port?
360 dockerComposeServices.add(svc);
367 * Shut everything down.
368 * @throws Exception but not really
371 public void close() throws Exception {
373 ProcUtils.runProcess(10000, downCmd);
378 tmpDockerComposeFile.delete();
379 } catch (Exception ignored) {
380 //No reason to fail the test, we're just being polite here.
385 * A thread that waits until it can "ping" a running OVS - tests basic reachability
386 * and readiness. The "ping" here is actually a list_dbs method and the response is
387 * checked to make sure the Open_Vswitch DB is present. Note that this thread will
388 * run until it succeeds unless its interrupt() method is called.
390 class OvsdbPing extends Thread {
392 private final String host;
393 private final int port;
394 private final AtomicInteger result;
395 public CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
396 ByteBuffer listDbsRequest;
399 * Construct a new OvsdbPing object.
400 * @param ovsNumber which OVS is this?
401 * @param result an AtomicInteger that is incremented upon a successful "ping"
403 public OvsdbPing(int ovsNumber, AtomicInteger result) {
404 this.host = getOvsdbAddress(ovsNumber);
405 this.port = Integer.parseInt(getOvsdbPort(ovsNumber));
406 this.result = result;
407 listDbsRequest = ByteBuffer.wrap(
408 ("{\"method\": \"list_dbs\", \"params\": [], \"id\": " + port + "}").getBytes());
409 listDbsRequest.mark();
417 } catch (InterruptedException e) {
418 LOG.warn("OvsdbPing interrupted", e);
425 * Attempt a "ping" of the OVSDB connection.
426 * @return true if the ping was successful OR IF THIS THREAD WAS INTERRUPTED
428 private boolean doPing() {
429 try (SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(host, port))) {
430 socketChannel.write(listDbsRequest);
431 listDbsRequest.reset();
433 ByteBuffer buf = ByteBuffer.allocateDirect(512);
434 socketChannel.read(buf);
436 String response = decoder.decode(buf).toString();
438 if (response.contains("Open_vSwitch")) {
439 LOG.info("OvsdbPing connection validated");
440 result.incrementAndGet();
443 } catch (ClosedByInterruptException e) {
444 LOG.warn("OvsdbPing interrupted", e);
445 //return true here because we're done, ne'er to return again.
447 } catch (Exception e) {
448 LOG.info("OvsdbPing exception while attempting connect {}", e.toString());
455 * Wait for all Ovs's to accept and respond to OVSDB requests.
456 * @param waitFor How long to wait
457 * @throws IOException if something goes wrong on the IO end
458 * @throws InterruptedException If this thread is interrupted
460 private void waitForOvsdbServers(long waitFor) throws IOException, InterruptedException {
461 AtomicInteger numRunningOvs = new AtomicInteger(0);
463 int numOvs = dockerComposeServices.size();
468 OvsdbPing[] pingers = new OvsdbPing[numOvs];
469 for (int i = 0; i < numOvs; i++) {
470 pingers[i] = new OvsdbPing(i, numRunningOvs);
474 long startTime = System.currentTimeMillis();
475 while ( (System.currentTimeMillis() - startTime) < waitFor) {
476 if (numRunningOvs.get() >= numOvs) {
477 LOG.info("DockerOvs.waitForOvsdbServers all OVS instances running");
482 LOG.info("DockerOvs.waitForOvsdbServers - finished waiting in {}", System.currentTimeMillis() - startTime);
484 for (OvsdbPing pinger : pingers) {
490 * Since the docker-compose file is a resource in the bundle and docker-compose needs it.
491 * in the file system, we copy it over - ugly but necessary.
492 * @param yamlFileName File name
493 * @return A File object for the newly created temporary yaml file.
495 private File createTempDockerComposeFile(String yamlFileName) {
496 Bundle bundle = FrameworkUtil.getBundle(this.getClass());
497 Assert.assertNotNull("DockerOvs: bundle is null", bundle);
498 URL url = bundle.getResource(envDockerComposeFile);
499 Assert.assertNotNull("DockerOvs: URL is null", url);
503 tmpFile = File.createTempFile("ovsdb-it-tmp-", null);
505 try (Reader in = new InputStreamReader(url.openStream());
506 FileWriter out = new FileWriter(tmpFile)) {
507 char[] buf = new char[1024];
509 while (-1 != (read = in.read(buf))) {
510 out.write(buf, 0, read);
514 } catch (IOException e) {
515 Assert.fail(e.toString());
522 * Useful for debugging. Dump some interesting config
523 * @throws IOException If something goes wrong with reading the process output
524 * @throws InterruptedException because there's some sleeping in here
526 public void logState(int dockerInstance) throws IOException, InterruptedException {
527 tryInContainer(5000, dockerInstance, "ip", "addr");
528 tryInContainer(5000, dockerInstance, "ovs-vsctl", "show");
529 tryInContainer(5000, dockerInstance, "ovs-ofctl", "-OOpenFlow13", "show", "br-int");
530 tryInContainer(5000, dockerInstance, "ovs-ofctl", "-OOpenFlow13", "dump-flows", "br-int");
531 tryInContainer(5000, dockerInstance, "ip", "netns", "list");